diff --git a/cmd/amazon-cloudwatch-agent-config-wizard/wizard.go b/cmd/amazon-cloudwatch-agent-config-wizard/wizard.go index 78b14edf55..efa70e92bd 100644 --- a/cmd/amazon-cloudwatch-agent-config-wizard/wizard.go +++ b/cmd/amazon-cloudwatch-agent-config-wizard/wizard.go @@ -4,139 +4,22 @@ package main import ( - "bufio" "flag" - "fmt" + "log" "os" - "github.com/aws/amazon-cloudwatch-agent/tool/data" - "github.com/aws/amazon-cloudwatch-agent/tool/processors" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/basicInfo" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/migration/linux" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/migration/windows" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/serialization" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/tracesconfig" - "github.com/aws/amazon-cloudwatch-agent/tool/runtime" - "github.com/aws/amazon-cloudwatch-agent/tool/stdin" - "github.com/aws/amazon-cloudwatch-agent/tool/testutil" - "github.com/aws/amazon-cloudwatch-agent/tool/util" + "github.com/aws/amazon-cloudwatch-agent/tool/cmdwrapper" + "github.com/aws/amazon-cloudwatch-agent/tool/wizard/flags" ) -type IMainProcessor interface { - VerifyProcessor(processor interface{}) -} -type MainProcessorStruct struct{} - -var MainProcessorGlobal IMainProcessor = &MainProcessorStruct{} - -var isNonInteractiveWindowsMigration *bool - -var configOutputPath *string - -var isNonInteractiveXrayMigration *bool - func main() { - // Parse command line args for non-interactive Windows migration - isNonInteractiveWindowsMigration = flag.Bool("isNonInteractiveWindowsMigration", false, - "If true, it will use command line args to bypass the wizard. Default value is false.") - - isNonInteractiveLinuxMigration := flag.Bool("isNonInteractiveLinuxMigration", false, - "If true, it will do the linux config migration. Default value is false.") - - tracesOnly := flag.Bool("tracesOnly", false, "If true, only trace configuration will be generated") - useParameterStore := flag.Bool("useParameterStore", false, - "If true, it will use the parameter store for the migrated config storage.") - isNonInteractiveXrayMigration = flag.Bool("nonInteractiveXrayMigration", false, "If true, then this is part of non Interactive xray migration tool.") - configFilePath := flag.String("configFilePath", "", - fmt.Sprintf("The path of the old config file. Default is %s on Windows or %s on Linux", windows.DefaultFilePathWindowsConfiguration, linux.DefaultFilePathLinuxConfiguration)) - - configOutputPath = flag.String("configOutputPath", "", "Specifies where to write the configuration file generated by the wizard") - parameterStoreName := flag.String("parameterStoreName", "", "The parameter store name. Default is AmazonCloudWatch-windows") - parameterStoreRegion := flag.String("parameterStoreRegion", "", "The parameter store region. Default is us-east-1") + log.Printf("Starting config-wizard, this will map back to a call to amazon-cloudwatch-agent") + translatorFlags := cmdwrapper.AddFlags("", flags.WizardFlags) flag.Parse() - if *isNonInteractiveWindowsMigration { - addWindowsMigrationInputs(*configFilePath, *parameterStoreName, *parameterStoreRegion, *useParameterStore) - } else if *isNonInteractiveLinuxMigration { - ctx := new(runtime.Context) - config := new(data.Config) - ctx.HasExistingLinuxConfig = true - ctx.ConfigFilePath = *configFilePath - if ctx.ConfigFilePath == "" { - ctx.ConfigFilePath = linux.DefaultFilePathLinuxConfiguration - } - process(ctx, config, linux.Processor, serialization.Processor) - return - } else if *tracesOnly { - ctx := new(runtime.Context) - config := new(data.Config) - ctx.TracesOnly = true - ctx.ConfigOutputPath = *configOutputPath - if *isNonInteractiveXrayMigration { - ctx.NonInteractiveXrayMigration = true - } - process(ctx, config, tracesconfig.Processor, serialization.Processor) - return - } - - startProcessing() -} - -func init() { - stdin.Scanln = func(a ...interface{}) (n int, err error) { - scanner := bufio.NewScanner(os.Stdin) - scanner.Scan() - if len(a) > 0 { - *a[0].(*string) = scanner.Text() - n = len(*a[0].(*string)) - } - err = scanner.Err() - return + err := cmdwrapper.ExecuteAgentCommand(flags.Command, translatorFlags) + if err != nil { + os.Exit(1) } - processors.StartProcessor = basicInfo.Processor -} - -func addWindowsMigrationInputs(configFilePath string, parameterStoreName string, parameterStoreRegion string, useParameterStore bool) { - inputChan := testutil.SetUpTestInputStream() - if useParameterStore { - testutil.Type(inputChan, "2", "1", "2", "1", configFilePath, "1", parameterStoreName, parameterStoreRegion, "1") - } else { - testutil.Type(inputChan, "2", "1", "2", "1", configFilePath, "2") - } -} - -func process(ctx *runtime.Context, config *data.Config, processors ...processors.Processor) { - for _, processor := range processors { - processor.Process(ctx, config) - } -} - -func startProcessing() { - ctx := new(runtime.Context) - config := new(data.Config) - ctx.ConfigOutputPath = *configOutputPath - var processor interface{} - processor = processors.StartProcessor - if *isNonInteractiveWindowsMigration { - ctx.WindowsNonInteractiveMigration = true - } - if *isNonInteractiveXrayMigration { - ctx.NonInteractiveXrayMigration = true - } - for { - if processor == nil { - if util.CurOS() == util.OsTypeWindows && !*isNonInteractiveWindowsMigration { - util.EnterToExit() - } - fmt.Println("Program exits now.") - break - } - MainProcessorGlobal.VerifyProcessor(processor) // For testing purposes - processor.(processors.Processor).Process(ctx, config) - processor = processor.(processors.Processor).NextProcessor(ctx, config) - } -} - -func (p *MainProcessorStruct) VerifyProcessor(processor interface{}) { } diff --git a/cmd/amazon-cloudwatch-agent-config-wizard/wizard_test.go b/cmd/amazon-cloudwatch-agent-config-wizard/wizard_test.go deleted file mode 100644 index de16cadf22..0000000000 --- a/cmd/amazon-cloudwatch-agent-config-wizard/wizard_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT - -package main - -import ( - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - - "github.com/aws/amazon-cloudwatch-agent/tool/processors" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/agentconfig" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/basicInfo" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/collectd" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/migration" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/migration/windows" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/ssm" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/statsd" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/template" - "github.com/aws/amazon-cloudwatch-agent/tool/processors/tracesconfig" - "github.com/aws/amazon-cloudwatch-agent/tool/util" -) - -type MainProcessorMock struct { - mock.Mock -} - -func (m *MainProcessorMock) VerifyProcessor(processor interface{}) { - m.Called(processor) -} - -func TestMainMethod(t *testing.T) { - processors.StartProcessor = template.Processor - main() -} - -func TestWindowsMigration(t *testing.T) { - // Do the mocking - processorMock := &MainProcessorMock{} - processorMock.On("VerifyProcessor", basicInfo.Processor).Return() - processorMock.On("VerifyProcessor", agentconfig.Processor).Return() - processorMock.On("VerifyProcessor", statsd.Processor).Return() - processorMock.On("VerifyProcessor", collectd.Processor).Return() - processorMock.On("VerifyProcessor", migration.Processor).Return() - processorMock.On("VerifyProcessor", tracesconfig.Processor).Return() - processorMock.On("VerifyProcessor", windows.Processor).Return() - processorMock.On("VerifyProcessor", ssm.Processor).Return() - MainProcessorGlobal = processorMock - - // Run the functions - absPath, _ := filepath.Abs("../../tool/processors/migration/windows/testData/input1.json") - addWindowsMigrationInputs(absPath, "", "", false) - processors.StartProcessor = basicInfo.Processor - - *isNonInteractiveWindowsMigration = true - startProcessing() - - // Assert expected behaviour - assert.True(t, processorMock.AssertNumberOfCalls(t, "VerifyProcessor", 7)) - - // Assert the resultant output file as well - absPath, _ = filepath.Abs("../../tool/processors/migration/windows/testData/output1.json") - expectedConfig, err := windows.ReadNewConfigFromPath(absPath) - if err != nil { - t.Error(err) - return - } - actualConfig, err := windows.ReadNewConfigFromPath(util.ConfigFilePath()) - if err != nil { - t.Error(err) - return - } - if !windows.AreTwoConfigurationsEqual(actualConfig, expectedConfig) { - t.Errorf("The generated new config is incorrect, got:\n '%v'\n, want:\n '%v'.\n", actualConfig, expectedConfig) - } -} diff --git a/cmd/amazon-cloudwatch-agent/amazon-cloudwatch-agent.go b/cmd/amazon-cloudwatch-agent/amazon-cloudwatch-agent.go index da084dedcd..5d34a92a0c 100644 --- a/cmd/amazon-cloudwatch-agent/amazon-cloudwatch-agent.go +++ b/cmd/amazon-cloudwatch-agent/amazon-cloudwatch-agent.go @@ -41,13 +41,20 @@ import ( "github.com/aws/amazon-cloudwatch-agent/internal/version" cwaLogger "github.com/aws/amazon-cloudwatch-agent/logger" "github.com/aws/amazon-cloudwatch-agent/logs" - _ "github.com/aws/amazon-cloudwatch-agent/plugins" + _ "github.com/aws/amazon-cloudwatch-agent/plugins" // do not remove, necessary for telegraf to know what plugins are used "github.com/aws/amazon-cloudwatch-agent/profiler" "github.com/aws/amazon-cloudwatch-agent/receiver/adapter" "github.com/aws/amazon-cloudwatch-agent/service/configprovider" "github.com/aws/amazon-cloudwatch-agent/service/defaultcomponents" "github.com/aws/amazon-cloudwatch-agent/service/registry" + "github.com/aws/amazon-cloudwatch-agent/tool/cmdwrapper" + "github.com/aws/amazon-cloudwatch-agent/tool/downloader" + downloaderflags "github.com/aws/amazon-cloudwatch-agent/tool/downloader/flags" "github.com/aws/amazon-cloudwatch-agent/tool/paths" + "github.com/aws/amazon-cloudwatch-agent/tool/wizard" + wizardflags "github.com/aws/amazon-cloudwatch-agent/tool/wizard/flags" + "github.com/aws/amazon-cloudwatch-agent/translator/cmdutil" + translatorflags "github.com/aws/amazon-cloudwatch-agent/translator/flags" "github.com/aws/amazon-cloudwatch-agent/translator/tocwconfig/toyamlconfig" ) @@ -95,6 +102,11 @@ var fRunAsConsole = flag.Bool("console", false, "run as console application (win var fSetEnv = flag.String("setenv", "", "set an env in the configuration file in the format of KEY=VALUE") var fStartUpErrorFile = flag.String("startup-error-file", "", "file to touch if agent can't start") +// sub-commands +var fConfigTranslator = flag.Bool(translatorflags.TranslatorCommand, false, "run in config-translator mode") +var fConfigDownloader = flag.Bool(downloaderflags.Command, false, "run in config-downloader mode") +var fConfigWizard = flag.Bool(wizardflags.Command, false, "run in config-wizard mode") + var stop chan struct{} func reloadLoop( @@ -485,6 +497,10 @@ func (p *program) Stop(_ service.Service) error { func main() { flag.Var(&fOtelConfigs, configprovider.OtelConfigFlagName, "YAML configuration files to run OTel pipeline") + translatorFlags := cmdwrapper.AddFlags(translatorflags.TranslatorCommand, translatorflags.TranslatorFlags) + downloaderFlags := cmdwrapper.AddFlags(downloaderflags.Command, downloaderflags.DownloaderFlags) + wizardFlags := cmdwrapper.AddFlags(wizardflags.Command, wizardflags.WizardFlags) + flag.Parse() if len(fOtelConfigs) == 0 { _ = fOtelConfigs.Set(paths.YamlConfigPath) @@ -607,6 +623,24 @@ func main() { } } return + case *fConfigTranslator: + err := cmdutil.RunTranslator(translatorFlags) + if err != nil { + log.Fatalf("E! Failed to initialize config translator: %v", err) + } + return + case *fConfigDownloader: + err := downloader.RunDownloaderFromFlags(downloaderFlags) + if err != nil { + log.Fatalf("E! Failed to initialize config downloader: %v", err) + } + return + case *fConfigWizard: + err := wizard.RunWizardFromFlags(wizardFlags) + if err != nil { + log.Fatalf("E! Failed to run config wizard: %v", err) + } + return } if runtime.GOOS == "windows" && windowsRunAsService() { diff --git a/cmd/config-downloader/downloader.go b/cmd/config-downloader/downloader.go index 21a8aa0338..9fa254f66f 100644 --- a/cmd/config-downloader/downloader.go +++ b/cmd/config-downloader/downloader.go @@ -1,229 +1,24 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT - package main import ( "flag" - "fmt" "log" "os" - "path/filepath" - "strings" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/ssm" - - configaws "github.com/aws/amazon-cloudwatch-agent/cfg/aws" - "github.com/aws/amazon-cloudwatch-agent/cfg/commonconfig" - "github.com/aws/amazon-cloudwatch-agent/internal/constants" - "github.com/aws/amazon-cloudwatch-agent/translator/config" - "github.com/aws/amazon-cloudwatch-agent/translator/util" - sdkutil "github.com/aws/amazon-cloudwatch-agent/translator/util" -) - -const ( - locationDefault = "default" - locationSSM = "ssm" - locationFile = "file" - - locationSeparator = ":" - exitErrorMessage = "Fail to fetch the config!" + "github.com/aws/amazon-cloudwatch-agent/tool/cmdwrapper" + "github.com/aws/amazon-cloudwatch-agent/tool/downloader/flags" ) -func defaultJsonConfig(mode string) (string, error) { - return config.DefaultJsonConfig(config.ToValidOs(""), mode), nil -} - -func downloadFromSSM(region, parameterStoreName, mode string, credsConfig map[string]string) (string, error) { - fmt.Printf("Region: %v\n", region) - fmt.Printf("credsConfig: %v\n", credsConfig) - var ses *session.Session - credsMap := util.GetCredentials(mode, credsConfig) - profile, profileOk := credsMap[commonconfig.CredentialProfile] - sharedConfigFile, sharedConfigFileOk := credsMap[commonconfig.CredentialFile] - rootconfig := &aws.Config{ - Region: aws.String(region), - LogLevel: configaws.SDKLogLevel(), - Logger: configaws.SDKLogger{}, - } - if profileOk || sharedConfigFileOk { - rootconfig.Credentials = credentials.NewCredentials(&credentials.SharedCredentialsProvider{ - Filename: sharedConfigFile, - Profile: profile, - }) - } - - ses, err := session.NewSession(rootconfig) - if err != nil { - fmt.Printf("Error in creating session: %v\n", err) - return "", err - } - - ssmClient := ssm.New(ses) - input := ssm.GetParameterInput{ - Name: aws.String(parameterStoreName), - WithDecryption: aws.Bool(true), - } - output, err := ssmClient.GetParameter(&input) - if err != nil { - fmt.Printf("Error in retrieving parameter store content: %v\n", err) - return "", err - } - - return *output.Parameter.Value, nil -} - -func readFromFile(filePath string) (string, error) { - bytes, err := os.ReadFile(filePath) - return string(bytes), err -} - -func EscapeFilePath(filePath string) (escapedFilePath string) { - escapedFilePath = filepath.ToSlash(filePath) - escapedFilePath = strings.Replace(escapedFilePath, "/", "_", -1) - escapedFilePath = strings.Replace(escapedFilePath, " ", "_", -1) - escapedFilePath = strings.Replace(escapedFilePath, ":", "_", -1) - return -} - -/** - * multi-config: - * default, append: download config to the dir and append .tmp suffix - * remove: remove the config from the dir - */ func main() { + log.Printf("Starting config-downloader, this will map back to a call to amazon-cloudwatch-agent") - defer func() { - if r := recover(); r != nil { - if val, ok := r.(string); ok { - fmt.Println(val) - } - fmt.Println(exitErrorMessage) - os.Exit(1) - } - }() - - var region, mode, downloadLocation, outputDir, inputConfig, multiConfig string - - flag.StringVar(&mode, "mode", "ec2", "Please provide the mode, i.e. ec2, onPremise, onPrem, auto") - flag.StringVar(&downloadLocation, "download-source", "", - "Download source. Example: \"ssm:my-parameter-store-name\" for an EC2 SSM Parameter Store Name holding your CloudWatch Agent configuration.") - flag.StringVar(&outputDir, "output-dir", "", "Path of output json config directory.") - flag.StringVar(&inputConfig, "config", "", "Please provide the common-config file") - flag.StringVar(&multiConfig, "multi-config", "default", "valid values: default, append, remove") + translatorFlags := cmdwrapper.AddFlags("", flags.DownloaderFlags) flag.Parse() - cc := commonconfig.New() - if inputConfig != "" { - f, err := os.Open(inputConfig) - if err != nil { - log.Panicf("E! Failed to open Common Config: %v", err) - } - - if err := cc.Parse(f); err != nil { - log.Panicf("E! Failed to open Common Config: %v", err) - } - } - util.SetProxyEnv(cc.ProxyMap()) - util.SetSSLEnv(cc.SSLMap()) - var errorMessage string - if downloadLocation == "" || outputDir == "" { - executable, err := os.Executable() - if err == nil { - errorMessage = fmt.Sprintf("E! usage: " + filepath.Base(executable) + " --output-dir --download-source ssm: ") - } else { - errorMessage = fmt.Sprintf("E! usage: --output-dir --download-source ssm: ") - } - log.Panicf(errorMessage) - } - - mode = sdkutil.DetectAgentMode(mode) - - region, _ = util.DetectRegion(mode, cc.CredentialsMap()) - - if region == "" && downloadLocation != locationDefault { - fmt.Println("Unable to determine aws-region.") - if mode == config.ModeEC2 { - errorMessage = "E! Please check if you can access the metadata service. For example, on linux, run 'wget -q -O - http://169.254.169.254/latest/meta-data/instance-id && echo' " - } else { - errorMessage = "E! Please make sure the credentials and region set correctly on your hosts.\n" + - "Refer to http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html" - } - log.Panicf(errorMessage) - } - - // clean up output dir for tmp files before writing out new tmp file. - // this step cannot be in translator because it is too late at that time. - filepath.Walk( - outputDir, - func(path string, info os.FileInfo, err error) error { - if err != nil { - fmt.Printf("Cannot access %v: %v \n", path, err) - return err - } - if info.IsDir() { - if strings.EqualFold(path, outputDir) { - return nil - } else { - fmt.Printf("Sub dir %v will be ignored.", path) - return filepath.SkipDir - } - } - if filepath.Ext(path) == constants.FileSuffixTmp { - return os.Remove(path) - } - return nil - }) - - locationArray := strings.SplitN(downloadLocation, locationSeparator, 2) - if locationArray == nil || len(locationArray) < 2 && downloadLocation != locationDefault { - log.Panicf("E! downloadLocation %s is malformated.", downloadLocation) - } - - var config, outputFilePath string - var err error - switch locationArray[0] { - case locationDefault: - outputFilePath = locationDefault - if multiConfig != "remove" { - config, err = defaultJsonConfig(mode) - } - case locationSSM: - outputFilePath = locationSSM + "_" + EscapeFilePath(locationArray[1]) - if multiConfig != "remove" { - config, err = downloadFromSSM(region, locationArray[1], mode, cc.CredentialsMap()) - } - case locationFile: - outputFilePath = locationFile + "_" + EscapeFilePath(filepath.Base(locationArray[1])) - if multiConfig != "remove" { - config, err = readFromFile(locationArray[1]) - } - default: - log.Panicf("E! Location type %s is not supported.", locationArray[0]) - } - + err := cmdwrapper.ExecuteAgentCommand(flags.Command, translatorFlags) if err != nil { - log.Panicf("E! Fail to fetch/remove json config: %v", err) - } - - if multiConfig != "remove" { - outputFilePath = filepath.Join(outputDir, outputFilePath+constants.FileSuffixTmp) - err = os.WriteFile(outputFilePath, []byte(config), 0644) - if err != nil { - log.Panicf("E! Failed to write the json file %v: %v", outputFilePath, err) - } else { - fmt.Printf("Successfully fetched the config and saved in %s\n", outputFilePath) - } - } else { - outputFilePath = filepath.Join(outputDir, outputFilePath) - if err := os.Remove(outputFilePath); err != nil { - log.Panicf("E! Failed to remove the json file %v: %v", outputFilePath, err) - } else { - fmt.Printf("Successfully removed the config file %s\n", outputFilePath) - } + os.Exit(1) } } diff --git a/cmd/config-translator/translator.go b/cmd/config-translator/translator.go index ad213dfd27..2426888c92 100644 --- a/cmd/config-translator/translator.go +++ b/cmd/config-translator/translator.go @@ -4,134 +4,22 @@ package main import ( - "errors" "flag" "log" "os" - "os/user" - "path/filepath" - "github.com/aws/amazon-cloudwatch-agent/cfg/commonconfig" - userutil "github.com/aws/amazon-cloudwatch-agent/internal/util/user" - "github.com/aws/amazon-cloudwatch-agent/translator" - "github.com/aws/amazon-cloudwatch-agent/translator/cmdutil" - "github.com/aws/amazon-cloudwatch-agent/translator/context" - "github.com/aws/amazon-cloudwatch-agent/translator/translate/otel/pipeline" - translatorUtil "github.com/aws/amazon-cloudwatch-agent/translator/util" + "github.com/aws/amazon-cloudwatch-agent/tool/cmdwrapper" + "github.com/aws/amazon-cloudwatch-agent/translator/flags" ) -const ( - exitErrorMessage = "Configuration validation first phase failed. Agent version: %v. Verify the JSON input is only using features supported by this version.\n" - exitSuccessMessage = "Configuration validation first phase succeeded" - version = "1.0" - envConfigFileName = "env-config.json" - yamlConfigFileName = "amazon-cloudwatch-agent.yaml" -) - -func initFlags() { - var inputOs = flag.String("os", "", "Please provide the os preference, valid value: windows/linux.") - var inputJsonFile = flag.String("input", "", "Please provide the path of input agent json config file") - var inputJsonDir = flag.String("input-dir", "", "Please provide the path of input agent json config directory.") - var inputTomlFile = flag.String("output", "", "Please provide the path of the output CWAgent config file") - var inputMode = flag.String("mode", "ec2", "Please provide the mode, i.e. ec2, onPremise, onPrem, auto") - var inputConfig = flag.String("config", "", "Please provide the common-config file") - var multiConfig = flag.String("multi-config", "remove", "valid values: default, append, remove") - flag.Parse() - - ctx := context.CurrentContext() - ctx.SetOs(*inputOs) - ctx.SetInputJsonFilePath(*inputJsonFile) - ctx.SetInputJsonDirPath(*inputJsonDir) - ctx.SetMultiConfig(*multiConfig) - ctx.SetOutputTomlFilePath(*inputTomlFile) - - if *inputConfig != "" { - f, err := os.Open(*inputConfig) - if err != nil { - log.Fatalf("E! Failed to open common-config file %s with error: %v", *inputConfig, err) - } - defer f.Close() - conf, err := commonconfig.Parse(f) - if err != nil { - log.Fatalf("E! Failed to parse common-config file %s with error: %v", *inputConfig, err) - } - ctx.SetCredentials(conf.CredentialsMap()) - ctx.SetProxy(conf.ProxyMap()) - ctx.SetSSL(conf.SSLMap()) - translatorUtil.LoadImdsRetries(conf.IMDS) - } - translatorUtil.SetProxyEnv(ctx.Proxy()) - translatorUtil.SetSSLEnv(ctx.SSL()) - - mode := translatorUtil.DetectAgentMode(*inputMode) - ctx.SetMode(mode) - ctx.SetKubernetesMode(translatorUtil.DetectKubernetesMode(mode)) -} - -/** - * config-translator --input ${JSON} --input-dir ${JSON_DIR} --output ${TOML} --mode ${param_mode} --config ${COMMON_CONFIG} - * --multi-config [default|append|remove] - * - * multi-config: - * default: only process .tmp files - * append: process both existing files and .tmp files - * remove: only process existing files - */ func main() { - initFlags() - defer func() { - if r := recover(); r != nil { - // Only emit error message if panic content is string(pre-checked) - // Not emitting the non-handled error message for now, we don't want to show non-user-friendly error message to customer - if val, ok := r.(string); ok { - log.Println(val) - } - //If the Input JSON config file is invalid, output all the error path and error messages. - for _, errMessage := range translator.ErrorMessages { - log.Println(errMessage) - } - log.Printf(exitErrorMessage, version) - os.Exit(1) - } - }() - ctx := context.CurrentContext() + log.Printf("Starting config-translator, this will map back to a call to amazon-cloudwatch-agent") - mergedJsonConfigMap, err := cmdutil.GenerateMergedJsonConfigMap(ctx) - if err != nil { - log.Panicf("E! Failed to generate merged json config: %v", err) - } - - if !ctx.RunInContainer() { - // run as user only applies to non container situation. - current, err := user.Current() - if err == nil && current.Name == "root" { - runAsUser, err := userutil.DetectRunAsUser(mergedJsonConfigMap) - if err != nil { - log.Panic("E! Failed to detectRunAsUser") - } - cmdutil.VerifyCredentials(ctx, runAsUser) - } - } + translatorFlags := cmdwrapper.AddFlags("", flags.TranslatorFlags) + flag.Parse() - tomlConfigPath := cmdutil.GetTomlConfigPath(ctx.OutputTomlFilePath()) - tomlConfigDir := filepath.Dir(tomlConfigPath) - yamlConfigPath := filepath.Join(tomlConfigDir, yamlConfigFileName) - tomlConfig, err := cmdutil.TranslateJsonMapToTomlConfig(mergedJsonConfigMap) + err := cmdwrapper.ExecuteAgentCommand(flags.TranslatorCommand, translatorFlags) if err != nil { - log.Panicf("E! Failed to generate TOML configuration validation content: %v", err) - } - yamlConfig, err := cmdutil.TranslateJsonMapToYamlConfig(mergedJsonConfigMap) - if err != nil && !errors.Is(err, pipeline.ErrNoPipelines) { - log.Panicf("E! Failed to generate YAML configuration validation content: %v", err) - } - if err = cmdutil.ConfigToTomlFile(tomlConfig, tomlConfigPath); err != nil { - log.Panicf("E! Failed to create the configuration TOML validation file: %v", err) - } - if err = cmdutil.ConfigToYamlFile(yamlConfig, yamlConfigPath); err != nil { - log.Panicf("E! Failed to create the configuration YAML validation file: %v", err) + os.Exit(1) } - log.Println(exitSuccessMessage) - // Put env config into the same folder as the toml config - envConfigPath := filepath.Join(tomlConfigDir, envConfigFileName) - cmdutil.TranslateJsonMapToEnvConfigFile(mergedJsonConfigMap, envConfigPath) } diff --git a/cmd/config-translator/translator_test.go b/cmd/config-translator/translator_test.go deleted file mode 100644 index b276fc4632..0000000000 --- a/cmd/config-translator/translator_test.go +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT - -package main - -import ( - "os" - "regexp" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/aws/amazon-cloudwatch-agent/translator/cmdutil" - "github.com/aws/amazon-cloudwatch-agent/translator/util" -) - -func checkIfSchemaValidateAsExpected(t *testing.T, jsonInputPath string, shouldSuccess bool, expectedErrorMap map[string]int) { - actualErrorMap := make(map[string]int) - - jsonInputMap, err := util.GetJsonMapFromFile(jsonInputPath) - if err != nil { - t.Fatalf("Failed to get json map from %v with error: %v", jsonInputPath, err) - } - - result, err := cmdutil.RunSchemaValidation(jsonInputMap) - if err != nil { - t.Fatalf("Failed to run schema validation: %v", err) - } - - if result.Valid() { - assert.True(t, shouldSuccess, "It should fail the schemaValidation!") - } else { - errorDetails := result.Errors() - for _, errorDetail := range errorDetails { - t.Logf("String: %v \n", errorDetail.String()) - t.Logf("Context: %v \n", errorDetail.Context().String()) - t.Logf("Description: %v \n", errorDetail.Description()) - t.Logf("Details: %v \n", errorDetail.Details()) - t.Logf("Field: %v \n", errorDetail.Field()) - t.Logf("Type: %v \n", errorDetail.Type()) - t.Logf("Value: %v \n", errorDetail.Value()) - if _, ok := actualErrorMap[errorDetail.Type()]; ok { - actualErrorMap[errorDetail.Type()] += 1 - } else { - actualErrorMap[errorDetail.Type()] = 1 - } - } - assert.Equal(t, expectedErrorMap, actualErrorMap, "Unexpected error set!") - assert.False(t, shouldSuccess, "It should pass the schemaValidation!") - } - -} - -func TestAgentConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validAgent.json", true, map[string]int{}) - expectedErrorMap := map[string]int{} - expectedErrorMap["invalid_type"] = 5 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidAgent.json", false, expectedErrorMap) -} - -func TestTracesConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validTrace.json", true, map[string]int{}) - expectedErrorMap := map[string]int{} - expectedErrorMap["array_min_properties"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidTrace.json", false, expectedErrorMap) -} - -func TestJMXConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validJMX.json", true, map[string]int{}) - expectedErrorMap := map[string]int{} - expectedErrorMap["additional_property_not_allowed"] = 1 - expectedErrorMap["number_any_of"] = 1 - expectedErrorMap["number_one_of"] = 1 - expectedErrorMap["required"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidJMX.json", false, expectedErrorMap) -} - -func TestOTLPMetricsConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validOTLPMetrics.json", true, map[string]int{}) - expectedErrorMap := map[string]int{} - expectedErrorMap["array_min_items"] = 1 - expectedErrorMap["number_one_of"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidOTLPMetrics.json", false, expectedErrorMap) -} - -func TestLogFilesConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validLogFiles.json", true, map[string]int{}) - expectedErrorMap := map[string]int{} - expectedErrorMap["array_min_items"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogFilesWithNoFileConfigured.json", false, expectedErrorMap) - expectedErrorMap1 := map[string]int{} - expectedErrorMap1["required"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogFilesWithMissingFilePath.json", false, expectedErrorMap1) - expectedErrorMap2 := map[string]int{} - expectedErrorMap2["unique"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogFilesWithDuplicateEntry.json", false, expectedErrorMap2) -} - -func TestLogWindowsEventConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validLogWindowsEvents.json", true, map[string]int{}) - expectedErrorMap := map[string]int{} - expectedErrorMap["number_not"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogWindowsEventsWithInvalidEventName.json", false, expectedErrorMap) - expectedErrorMap1 := map[string]int{} - expectedErrorMap1["required"] = 2 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogWindowsEventsWithMissingEventNameAndLevel.json", false, expectedErrorMap1) - expectedErrorMap2 := map[string]int{} - expectedErrorMap2["invalid_type"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogWindowsEventsWithInvalidEventLevelType.json", false, expectedErrorMap2) - expectedErrorMap3 := map[string]int{} - expectedErrorMap3["enum"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogWindowsEventsWithInvalidEventFormatType.json", false, expectedErrorMap3) -} - -func TestMetricsConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validLinuxMetrics.json", true, map[string]int{}) - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validWindowsMetrics.json", true, map[string]int{}) - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validMetricsWithAppSignals.json", true, map[string]int{}) - expectedErrorMap := map[string]int{} - expectedErrorMap["invalid_type"] = 2 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithInvalidAggregationDimensions.json", false, expectedErrorMap) - expectedErrorMap1 := map[string]int{} - expectedErrorMap1["array_min_properties"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithNoMetricsDefined.json", false, expectedErrorMap1) - expectedErrorMap2 := map[string]int{} - expectedErrorMap2["required"] = 1 - expectedErrorMap2["invalid_type"] = 2 - expectedErrorMap2["number_one_of"] = 2 - expectedErrorMap2["number_all_of"] = 3 - expectedErrorMap2["unique"] = 1 - expectedErrorMap2["number_gte"] = 1 - expectedErrorMap2["string_gte"] = 2 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithInvalidMeasurement.json", false, expectedErrorMap2) - expectedErrorMap3 := map[string]int{} - expectedErrorMap3["invalid_type"] = 2 - expectedErrorMap3["number_all_of"] = 2 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithInvalidAppendDimensions.json", false, expectedErrorMap3) - expectedErrorMap4 := map[string]int{} - expectedErrorMap4["enum"] = 1 - expectedErrorMap4["array_max_items"] = 1 - expectedErrorMap4["invalid_type"] = 1 - expectedErrorMap4["number_all_of"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithinvalidMetricsCollected.json", false, expectedErrorMap4) - expectedErrorMap5 := map[string]int{} - expectedErrorMap5["additional_property_not_allowed"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithAdditionalProperties.json", false, expectedErrorMap5) - expectedErrorMap6 := map[string]int{} - expectedErrorMap6["required"] = 1 - expectedErrorMap6["invalid_type"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithInvalidMetrics_Collected.json", false, expectedErrorMap6) -} - -func TestProcstatConfig(t *testing.T) { - expectedErrorMap := map[string]int{} - expectedErrorMap["invalid_type"] = 1 - expectedErrorMap["number_all_of"] = 1 - expectedErrorMap["number_any_of"] = 1 - expectedErrorMap["required"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidProcstatMeasurement.json", false, expectedErrorMap) - - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validProcstatConfig.json", true, map[string]int{}) -} - -func TestEthtoolConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validEthtoolConfig.json", true, map[string]int{}) -} - -func TestNvidiaGpuConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validNvidiaGpuConfig.json", true, map[string]int{}) -} - -func TestValidLogFilterConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validLogFilesWithFilters.json", true, map[string]int{}) -} - -func TestInvalidLogFilterConfig(t *testing.T) { - expectedErrorMap := map[string]int{ - "additional_property_not_allowed": 1, - "enum": 1, - } - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogFilesWithFilters.json", false, expectedErrorMap) -} - -func TestMetricsDestinationsConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validMetricsDestinations.json", true, map[string]int{}) - expectedErrorMap := map[string]int{} - expectedErrorMap["required"] = 1 - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsDestinations.json", false, expectedErrorMap) -} -func TestContainerInsightsJmxConfig(t *testing.T) { - checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validContainerInsightsJmx.json", true, map[string]int{}) -} - -// Validate all sampleConfig files schema -func TestSampleConfigSchema(t *testing.T) { - if files, err := os.ReadDir("../../translator/tocwconfig/sampleConfig/"); err == nil { - re := regexp.MustCompile(".json") - for _, file := range files { - if re.MatchString(file.Name()) { - t.Logf("Validating ../../translator/tocwconfig/sampleConfig/%s\n", file.Name()) - checkIfSchemaValidateAsExpected(t, "../../translator/tocwconfig/sampleConfig/"+file.Name(), true, map[string]int{}) - t.Logf("Validated ../../translator/tocwconfig/sampleConfig/%s\n", file.Name()) - } - } - } else { - panic(err) - } -} diff --git a/tool/cmdwrapper/cmdwrapper.go b/tool/cmdwrapper/cmdwrapper.go new file mode 100644 index 0000000000..161ba5c1dc --- /dev/null +++ b/tool/cmdwrapper/cmdwrapper.go @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package cmdwrapper + +import ( + "errors" + "flag" + "fmt" + "log" + "os" + "os/exec" + + "github.com/aws/amazon-cloudwatch-agent/tool/paths" +) + +type Flag struct { + DefaultValue string + Description string +} + +const delimiter = "-" + +// Make execCommand a variable that can be replaced in tests +var execCommand = exec.Command + +func AddFlags(prefix string, flagConfigs map[string]Flag) map[string]*string { + flags := make(map[string]*string) + for key, flagConfig := range flagConfigs { + flagName := key + if prefix != "" { + flagName = prefix + delimiter + flagName + } + flags[key] = flag.String(flagName, flagConfig.DefaultValue, flagConfig.Description) + } + return flags +} + +func ExecuteAgentCommand(command string, flags map[string]*string) error { + args := []string{fmt.Sprintf("-%s", command)} + + for key, value := range flags { + if *value != "" { + args = append(args, fmt.Sprintf("-%s%s%s", command, delimiter, key), *value) + } + } + + log.Printf("Executing %s with arguments: %v", paths.AgentBinaryPath, args) + + // Use execCommand instead of exec.Command directly + cmd := execCommand(paths.AgentBinaryPath, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + if err := cmd.Run(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + log.Panicf("E! Translation process exited with non-zero status: %d, err: %v", + exitErr.ExitCode(), exitErr) + } + log.Panicf("E! Translation process failed. Error: %v", err) + return err + } + + return nil +} diff --git a/tool/cmdwrapper/cmdwrapper_test.go b/tool/cmdwrapper/cmdwrapper_test.go new file mode 100644 index 0000000000..7b7797d61a --- /dev/null +++ b/tool/cmdwrapper/cmdwrapper_test.go @@ -0,0 +1,134 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package cmdwrapper + +import ( + "flag" + "os/exec" + "testing" + + "github.com/aws/amazon-cloudwatch-agent/tool/paths" +) + +func TestAddFlags(t *testing.T) { + // Reset the flag package to avoid conflicts + flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError) + + tests := []struct { + name string + prefix string + flagConfigs map[string]Flag + want map[string]string // Expected default values + }{ + { + name: "no prefix", + prefix: "", + flagConfigs: map[string]Flag{ + "test": { + DefaultValue: "default", + Description: "test description", + }, + }, + want: map[string]string{ + "test": "default", + }, + }, + { + name: "with prefix", + prefix: "prefix", + flagConfigs: map[string]Flag{ + "test": { + DefaultValue: "default", + Description: "test description", + }, + }, + want: map[string]string{ + "test": "default", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := AddFlags(tt.prefix, tt.flagConfigs) + + // Verify the returned map has the correct keys + if len(got) != len(tt.want) { + t.Errorf("AddFlags() returned map of size %d, want %d", len(got), len(tt.want)) + } + + // Verify default values + for key, wantValue := range tt.want { + if gotFlag, exists := got[key]; !exists { + t.Errorf("AddFlags() missing key %s", key) + } else if *gotFlag != wantValue { + t.Errorf("AddFlags() for key %s = %v, want %v", key, *gotFlag, wantValue) + } + } + }) + } +} + +func TestExecuteAgentCommand_HappyPath(t *testing.T) { + // Save the original execCommand and restore it after the test + originalExecCommand := execCommand + defer func() { execCommand = originalExecCommand }() + + var capturedPath string + var capturedArgs []string + + // Mock execCommand + execCommand = func(path string, args ...string) *exec.Cmd { + capturedPath = path + capturedArgs = args + + // Use "echo" as a no-op command that will succeed + cmd := exec.Command("echo", "1") + return cmd + } + + // Test data + command := "fetch-config" + flags := map[string]*string{ + "config": stringPtr("config-value"), + "mode": stringPtr("mode-value"), + } + + // Execute the function + err := ExecuteAgentCommand(command, flags) + + // Verify no error occurred + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // Verify the binary path + if capturedPath != paths.AgentBinaryPath { + t.Errorf("Expected binary path %s, got %s", paths.AgentBinaryPath, capturedPath) + } + + // Expected arguments + expectedArgs := []string{ + "-fetch-config", + "-fetch-config-config", "config-value", + "-fetch-config-mode", "mode-value", + } + + // Verify arguments length + if len(capturedArgs) != len(expectedArgs) { + t.Errorf("Expected %d arguments, got %d", len(expectedArgs), len(capturedArgs)) + } + + // Verify each argument + for i, expected := range expectedArgs { + if capturedArgs[i] != expected { + t.Errorf("Argument %d: expected %s, got %s", i, expected, capturedArgs[i]) + } + } +} + +// Helper function to create string pointer +func stringPtr(s string) *string { + return &s +} diff --git a/tool/downloader/downloader.go b/tool/downloader/downloader.go new file mode 100644 index 0000000000..816da9efcc --- /dev/null +++ b/tool/downloader/downloader.go @@ -0,0 +1,207 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package downloader + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ssm" + + configaws "github.com/aws/amazon-cloudwatch-agent/cfg/aws" + "github.com/aws/amazon-cloudwatch-agent/cfg/commonconfig" + "github.com/aws/amazon-cloudwatch-agent/internal/constants" + "github.com/aws/amazon-cloudwatch-agent/translator/config" + "github.com/aws/amazon-cloudwatch-agent/translator/util" + sdkutil "github.com/aws/amazon-cloudwatch-agent/translator/util" +) + +const ( + locationDefault = "default" + locationSSM = "ssm" + locationFile = "file" + + locationSeparator = ":" +) + +func RunDownloaderFromFlags(flags map[string]*string) error { + return RunDownloader( + *flags["mode"], + *flags["download-source"], + *flags["output-dir"], + *flags["config"], + *flags["multi-config"], + ) +} + +func RunDownloader(mode, downloadLocation, outputDir, inputConfig, multiConfig string) error { + defer func() { + if r := recover(); r != nil { + fmt.Println("Fail to fetch the config!") + os.Exit(1) + } + }() + + cc := commonconfig.New() + if inputConfig != "" { + f, err := os.Open(inputConfig) + if err != nil { + return fmt.Errorf("failed to open Common Config: %v", err) + } + defer f.Close() + + if err := cc.Parse(f); err != nil { + return fmt.Errorf("failed to parse Common Config: %v", err) + } + } + + // Set proxy and SSL environment + util.SetProxyEnv(cc.ProxyMap()) + util.SetSSLEnv(cc.SSLMap()) + + // Validate required parameters + if downloadLocation == "" || outputDir == "" { + executable, err := os.Executable() + if err == nil { + return fmt.Errorf("usage: %s --output-dir --download-source ssm:", + filepath.Base(executable)) + } + return fmt.Errorf("usage: --output-dir --download-source ssm:") + } + + // Detect agent mode and region + mode = sdkutil.DetectAgentMode(mode) + region, _ := util.DetectRegion(mode, cc.CredentialsMap()) + if region == "" && downloadLocation != locationDefault { + if mode == config.ModeEC2 { + return fmt.Errorf("please check if you can access the metadata service. For example, on linux, run 'wget -q -O - http://169.254.169.254/latest/meta-data/instance-id && echo'") + } + return fmt.Errorf("please make sure the credentials and region set correctly on your hosts") + } + + err := cleanupOutputDir(outputDir) + if err != nil { + return fmt.Errorf("failed to clean up output directory: %v", err) + } + + locationArray := strings.SplitN(downloadLocation, locationSeparator, 2) + if locationArray == nil || len(locationArray) < 2 && downloadLocation != locationDefault { + return fmt.Errorf("downloadLocation %s is malformed", downloadLocation) + } + + var config, outputFilePath string + switch locationArray[0] { + case locationDefault: + outputFilePath = locationDefault + if multiConfig != "remove" { + config, err = defaultJSONConfig(mode) + } + case locationSSM: + outputFilePath = locationSSM + "_" + EscapeFilePath(locationArray[1]) + if multiConfig != "remove" { + config, err = downloadFromSSM(region, locationArray[1], mode, cc.CredentialsMap()) + } + case locationFile: + outputFilePath = locationFile + "_" + EscapeFilePath(filepath.Base(locationArray[1])) + if multiConfig != "remove" { + config, err = readFromFile(locationArray[1]) + } + default: + return fmt.Errorf("location type %s is not supported", locationArray[0]) + } + + if err != nil { + return fmt.Errorf("fail to fetch/remove json config: %v", err) + } + + if multiConfig != "remove" { + outputPath := filepath.Join(outputDir, outputFilePath+constants.FileSuffixTmp) + // #nosec G306 - customers may need to be able to read the config file that the downloader downloaded for them + if err := os.WriteFile(outputPath, []byte(config), 0644); err != nil { + return fmt.Errorf("failed to write the json file %v: %v", outputPath, err) + } + } else { + outputPath := filepath.Join(outputDir, outputFilePath) + if err := os.Remove(outputPath); err != nil { + return fmt.Errorf("failed to remove the json file %v: %v", outputPath, err) + } + } + + return nil +} + +func defaultJSONConfig(mode string) (string, error) { + return config.DefaultJsonConfig(config.ToValidOs(""), mode), nil +} + +func downloadFromSSM(region, parameterStoreName, mode string, credsConfig map[string]string) (string, error) { + var ses *session.Session + credsMap := util.GetCredentials(mode, credsConfig) + profile, profileOk := credsMap[commonconfig.CredentialProfile] + sharedConfigFile, sharedConfigFileOk := credsMap[commonconfig.CredentialFile] + rootconfig := &aws.Config{ + Region: aws.String(region), + LogLevel: configaws.SDKLogLevel(), + Logger: configaws.SDKLogger{}, + } + if profileOk || sharedConfigFileOk { + rootconfig.Credentials = credentials.NewCredentials(&credentials.SharedCredentialsProvider{ + Filename: sharedConfigFile, + Profile: profile, + }) + } + + ses, err := session.NewSession(rootconfig) + if err != nil { + return "", fmt.Errorf("error in creating session: %v", err) + } + + ssmClient := ssm.New(ses) + input := ssm.GetParameterInput{ + Name: aws.String(parameterStoreName), + WithDecryption: aws.Bool(true), + } + output, err := ssmClient.GetParameter(&input) + if err != nil { + return "", fmt.Errorf("error in retrieving parameter store content: %v", err) + } + + return *output.Parameter.Value, nil +} + +func readFromFile(filePath string) (string, error) { + bytes, err := os.ReadFile(filePath) + return string(bytes), err +} + +func EscapeFilePath(filePath string) string { + escapedFilePath := filepath.ToSlash(filePath) + escapedFilePath = strings.Replace(escapedFilePath, "/", "_", -1) + escapedFilePath = strings.Replace(escapedFilePath, " ", "_", -1) + escapedFilePath = strings.Replace(escapedFilePath, ":", "_", -1) + return escapedFilePath +} + +func cleanupOutputDir(outputDir string) error { + return filepath.Walk(outputDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("cannot access %v: %v", path, err) + } + if info.IsDir() { + if strings.EqualFold(path, outputDir) { + return nil + } + return filepath.SkipDir + } + if filepath.Ext(path) == constants.FileSuffixTmp { + return os.Remove(path) + } + return nil + }) +} diff --git a/tool/downloader/flags/flags.go b/tool/downloader/flags/flags.go new file mode 100644 index 0000000000..2fb06cfa9c --- /dev/null +++ b/tool/downloader/flags/flags.go @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package flags + +import "github.com/aws/amazon-cloudwatch-agent/tool/cmdwrapper" + +const Command = "config-downloader" + +var DownloaderFlags = map[string]cmdwrapper.Flag{ + "mode": {DefaultValue: "ec2", Description: "Please provide the mode, i.e. ec2, onPremise, onPrem, auto"}, + "download-source": {DefaultValue: "", Description: "Download source. Example: \"ssm:my-parameter-store-name\" for an EC2 SSM Parameter Store Name holding your CloudWatch Agent configuration."}, + "output-dir": {DefaultValue: "", Description: "Path of output json config directory."}, + "config": {DefaultValue: "", Description: "Please provide the common-config file"}, + "multi-config": {DefaultValue: "default", Description: "valid values: default, append, remove"}, +} diff --git a/tool/processors/migration/linux/linuxMigration.go b/tool/processors/migration/linux/linuxMigration.go index 1f8b50ef96..36af502a00 100644 --- a/tool/processors/migration/linux/linuxMigration.go +++ b/tool/processors/migration/linux/linuxMigration.go @@ -14,13 +14,13 @@ import ( "github.com/aws/amazon-cloudwatch-agent/tool/processors/question/logs" "github.com/aws/amazon-cloudwatch-agent/tool/runtime" "github.com/aws/amazon-cloudwatch-agent/tool/util" + "github.com/aws/amazon-cloudwatch-agent/tool/wizard/flags" ) const ( - genericSectionName = "general" - anyExistingLinuxConfigQuestion = "Do you have any existing CloudWatch Log Agent (http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html) configuration file to import for migration?" - filePathLinuxConfigQuestion = "What is the file path for the existing cloudwatch log agent configuration file?" - DefaultFilePathLinuxConfiguration = "/var/awslogs/etc/awslogs.conf" + genericSectionName = "general" + anyExistingLinuxConfigQuestion = "Do you have any existing CloudWatch Log Agent (http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html) configuration file to import for migration?" + filePathLinuxConfigQuestion = "What is the file path for the existing cloudwatch log agent configuration file?" ) var Processor processors.Processor = &processor{} @@ -31,7 +31,7 @@ func (p *processor) Process(ctx *runtime.Context, config *data.Config) { if ctx.HasExistingLinuxConfig || util.No(anyExistingLinuxConfigQuestion) { filePath := ctx.ConfigFilePath if filePath == "" { - filePath = util.AskWithDefault(filePathLinuxConfigQuestion, DefaultFilePathLinuxConfiguration) + filePath = util.AskWithDefault(filePathLinuxConfigQuestion, flags.DefaultFilePathLinuxConfiguration) } processConfigFromPythonConfigParserFile(filePath, config.LogsConf()) } diff --git a/tool/processors/migration/linux/linuxMigration_test.go b/tool/processors/migration/linux/linuxMigration_test.go index 4457d7ea09..49c200ad52 100644 --- a/tool/processors/migration/linux/linuxMigration_test.go +++ b/tool/processors/migration/linux/linuxMigration_test.go @@ -15,6 +15,7 @@ import ( "github.com/aws/amazon-cloudwatch-agent/tool/runtime" "github.com/aws/amazon-cloudwatch-agent/tool/testutil" "github.com/aws/amazon-cloudwatch-agent/tool/util" + "github.com/aws/amazon-cloudwatch-agent/tool/wizard/flags" ) func TestProcessor_Process(t *testing.T) { @@ -84,8 +85,8 @@ func TestFilePathForTheExistingConfigFile(t *testing.T) { inputChan := testutil.SetUpTestInputStream() testutil.Type(inputChan, "", "/var/test.conf") - assert.Equal(t, "/var/awslogs/etc/awslogs.conf", util.AskWithDefault(filePathLinuxConfigQuestion, DefaultFilePathLinuxConfiguration)) - assert.Equal(t, "/var/test.conf", util.AskWithDefault(filePathLinuxConfigQuestion, DefaultFilePathLinuxConfiguration)) + assert.Equal(t, "/var/awslogs/etc/awslogs.conf", util.AskWithDefault(filePathLinuxConfigQuestion, flags.DefaultFilePathLinuxConfiguration)) + assert.Equal(t, "/var/test.conf", util.AskWithDefault(filePathLinuxConfigQuestion, flags.DefaultFilePathLinuxConfiguration)) } func TestProcessConfigFromPythonConfigParserFile(t *testing.T) { diff --git a/tool/processors/migration/windows/windows_migration.go b/tool/processors/migration/windows/windows_migration.go index 86eaeedd69..f710000932 100644 --- a/tool/processors/migration/windows/windows_migration.go +++ b/tool/processors/migration/windows/windows_migration.go @@ -14,6 +14,7 @@ import ( "github.com/aws/amazon-cloudwatch-agent/tool/processors/ssm" "github.com/aws/amazon-cloudwatch-agent/tool/runtime" "github.com/aws/amazon-cloudwatch-agent/tool/util" + wizardflags "github.com/aws/amazon-cloudwatch-agent/tool/wizard/flags" ) var Processor processors.Processor = &processor{} @@ -21,9 +22,8 @@ var Processor processors.Processor = &processor{} type processor struct{} const ( - anyExistingLinuxConfigQuestion = "Do you have any existing CloudWatch Log Agent configuration file to import for migration?" - filePathWindowsConfigQuestion = "What is the file path for the existing Windows CloudWatch log agent configuration file?" - DefaultFilePathWindowsConfiguration = "C:\\Program Files\\Amazon\\SSM\\Plugins\\awsCloudWatch\\AWS.EC2.Windows.CloudWatch.json" + anyExistingLinuxConfigQuestion = "Do you have any existing CloudWatch Log Agent configuration file to import for migration?" + filePathWindowsConfigQuestion = "What is the file path for the existing Windows CloudWatch log agent configuration file?" ) func (p *processor) Process(ctx *runtime.Context, config *data.Config) { @@ -40,7 +40,7 @@ func (p *processor) NextProcessor(ctx *runtime.Context, config *data.Config) int func migrateOldAgentConfig() { // 1 - parse the old config var oldConfig OldSsmCwConfig - absPath := util.AskWithDefault(filePathWindowsConfigQuestion, DefaultFilePathWindowsConfiguration) + absPath := util.AskWithDefault(filePathWindowsConfigQuestion, wizardflags.DefaultFilePathWindowsConfiguration) if file, err := os.ReadFile(absPath); err == nil { if err := json.Unmarshal(file, &oldConfig); err != nil { fmt.Fprintf(os.Stderr, "Failed to parse the provided configuration file. Error details: %v", err) diff --git a/tool/wizard/flags/flags.go b/tool/wizard/flags/flags.go new file mode 100644 index 0000000000..95223047f3 --- /dev/null +++ b/tool/wizard/flags/flags.go @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package flags + +import ( + "fmt" + + "github.com/aws/amazon-cloudwatch-agent/tool/cmdwrapper" +) + +// this package needs to be separate to keep the binary size of the wizard small + +const ( + Command = "config-wizard" + DefaultFilePathWindowsConfiguration = "C:\\Program Files\\Amazon\\SSM\\Plugins\\awsCloudWatch\\AWS.EC2.Windows.CloudWatch.json" + DefaultFilePathLinuxConfiguration = "/var/awslogs/etc/awslogs.conf" +) + +var WizardFlags = map[string]cmdwrapper.Flag{ + "is-non-interactive-windows-migration": {DefaultValue: "false", Description: "If true, it will use command line args to bypass the wizard. Default value is false."}, + "is-non-interactive-linux-migration": {DefaultValue: "false", Description: "If true, it will do the linux config migration. Default value is false."}, + "traces-only": {DefaultValue: "false", Description: "If true, only trace configuration will be generated"}, + "use-parameter-store": {DefaultValue: "false", Description: "If true, it will use the parameter store for the migrated config storage."}, + "non-interactive-xray-migration": {DefaultValue: "false", Description: "If true, then this is part of non Interactive xray migration tool."}, + "config-file-path": {DefaultValue: "", Description: fmt.Sprintf("The path of the old config file. Default is %s on Windows or %s on Linux", DefaultFilePathWindowsConfiguration, DefaultFilePathLinuxConfiguration)}, + "config-output-path": {DefaultValue: "", Description: "Specifies where to write the configuration file generated by the wizard"}, + "parameter-store-name": {DefaultValue: "", Description: "The parameter store name. Default is AmazonCloudWatch-windows"}, + "parameter-store-region": {DefaultValue: "", Description: "The parameter store region. Default is us-east-1"}, +} diff --git a/tool/wizard/wizard.go b/tool/wizard/wizard.go new file mode 100644 index 0000000000..26d6947895 --- /dev/null +++ b/tool/wizard/wizard.go @@ -0,0 +1,147 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package wizard + +import ( + "bufio" + "fmt" + "os" + + "github.com/aws/amazon-cloudwatch-agent/tool/data" + "github.com/aws/amazon-cloudwatch-agent/tool/processors" + "github.com/aws/amazon-cloudwatch-agent/tool/processors/basicInfo" + "github.com/aws/amazon-cloudwatch-agent/tool/processors/migration/linux" + "github.com/aws/amazon-cloudwatch-agent/tool/processors/serialization" + "github.com/aws/amazon-cloudwatch-agent/tool/processors/tracesconfig" + "github.com/aws/amazon-cloudwatch-agent/tool/runtime" + "github.com/aws/amazon-cloudwatch-agent/tool/stdin" + "github.com/aws/amazon-cloudwatch-agent/tool/testutil" + "github.com/aws/amazon-cloudwatch-agent/tool/util" + wizardflags "github.com/aws/amazon-cloudwatch-agent/tool/wizard/flags" +) + +type IMainProcessor interface { + VerifyProcessor(_processor interface{}) +} + +type MainProcessorStruct struct{} + +var MainProcessorGlobal IMainProcessor = &MainProcessorStruct{} + +type Params struct { + IsNonInteractiveWindowsMigration bool + IsNonInteractiveLinuxMigration bool + TracesOnly bool + UseParameterStore bool + IsNonInteractiveXrayMigration bool + ConfigFilePath string + ConfigOutputPath string + ParameterStoreName string + ParameterStoreRegion string +} + +func init() { + stdin.Scanln = func(a ...interface{}) (int, error) { + var n int + var err error + + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + if len(a) > 0 { + *a[0].(*string) = scanner.Text() + n = len(*a[0].(*string)) + } + err = scanner.Err() + return n, err + } + processors.StartProcessor = basicInfo.Processor +} + +func RunWizard(params Params) error { + if params.IsNonInteractiveWindowsMigration { + addWindowsMigrationInputs( + params.ConfigFilePath, + params.ParameterStoreName, + params.ParameterStoreRegion, + params.UseParameterStore, + ) + } else if params.IsNonInteractiveLinuxMigration { + ctx := new(runtime.Context) + config := new(data.Config) + ctx.HasExistingLinuxConfig = true + ctx.ConfigFilePath = params.ConfigFilePath + if ctx.ConfigFilePath == "" { + ctx.ConfigFilePath = wizardflags.DefaultFilePathLinuxConfiguration + } + process(ctx, config, linux.Processor, serialization.Processor) + return nil + } else if params.TracesOnly { + ctx := new(runtime.Context) + config := new(data.Config) + ctx.TracesOnly = true + ctx.ConfigOutputPath = params.ConfigOutputPath + ctx.NonInteractiveXrayMigration = params.IsNonInteractiveXrayMigration + process(ctx, config, tracesconfig.Processor, serialization.Processor) + return nil + } + + startProcessing(params.ConfigOutputPath, params.IsNonInteractiveWindowsMigration, params.IsNonInteractiveXrayMigration) + return nil +} + +func RunWizardFromFlags(flags map[string]*string) error { + params := Params{ + IsNonInteractiveWindowsMigration: *flags["is-non-interactive-windows-migration"] == "true", + IsNonInteractiveLinuxMigration: *flags["is-non-interactive-linux-migration"] == "true", + TracesOnly: *flags["traces-only"] == "true", + UseParameterStore: *flags["use-parameter-store"] == "true", + IsNonInteractiveXrayMigration: *flags["non-interactive-xray-migration"] == "true", + ConfigFilePath: *flags["config-file-path"], + ConfigOutputPath: *flags["config-output-path"], + ParameterStoreName: *flags["parameter-store-name"], + ParameterStoreRegion: *flags["parameter-store-region"], + } + return RunWizard(params) +} + +func addWindowsMigrationInputs(configFilePath string, parameterStoreName string, parameterStoreRegion string, useParameterStore bool) { + inputChan := testutil.SetUpTestInputStream() + if useParameterStore { + testutil.Type(inputChan, "2", "1", "2", "1", configFilePath, "1", parameterStoreName, parameterStoreRegion, "1") + } else { + testutil.Type(inputChan, "2", "1", "2", "1", configFilePath, "2") + } +} + +func process(ctx *runtime.Context, config *data.Config, processors ...processors.Processor) { + for _, processor := range processors { + processor.Process(ctx, config) + } +} + +func startProcessing(configOutputPath string, isNonInteractiveWindowsMigration, isNonInteractiveXrayMigration bool) { + ctx := &runtime.Context{ + ConfigOutputPath: configOutputPath, + WindowsNonInteractiveMigration: isNonInteractiveWindowsMigration, + NonInteractiveXrayMigration: isNonInteractiveXrayMigration, + } + config := &data.Config{} + var processor interface{} + processor = processors.StartProcessor + for { + if processor == nil { + if util.CurOS() == util.OsTypeWindows && !isNonInteractiveWindowsMigration { + util.EnterToExit() + } + fmt.Println("Program exits now.") + break + } + MainProcessorGlobal.VerifyProcessor(processor) // For testing purposes + processor.(processors.Processor).Process(ctx, config) + processor = processor.(processors.Processor).NextProcessor(ctx, config) + } +} + +func (p *MainProcessorStruct) VerifyProcessor(interface{}) { +} diff --git a/tool/wizard/wizard_test.go b/tool/wizard/wizard_test.go new file mode 100644 index 0000000000..35116b5f06 --- /dev/null +++ b/tool/wizard/wizard_test.go @@ -0,0 +1,85 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package wizard + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/aws/amazon-cloudwatch-agent/tool/processors" + "github.com/aws/amazon-cloudwatch-agent/tool/processors/basicInfo" + "github.com/aws/amazon-cloudwatch-agent/tool/processors/migration/windows" + "github.com/aws/amazon-cloudwatch-agent/tool/util" +) + +type MainProcessorMock struct { + mock.Mock +} + +func (m *MainProcessorMock) VerifyProcessor(processor interface{}) { + m.Called(processor) +} + +func TestWindowsMigration(t *testing.T) { + // Do the mocking + processorMock := &MainProcessorMock{} + processorMock.On("VerifyProcessor", mock.Anything).Return() + MainProcessorGlobal = processorMock + + // Set up the test input file path + absPath, err := filepath.Abs("../../tool/processors/migration/windows/testData/input1.json") + assert.NoError(t, err, "Failed to get absolute path for input file") + + // Verify that the input file exists + _, err = os.Stat(absPath) + assert.NoError(t, err, "Input file does not exist: %s", absPath) + + // Run the wizard + params := Params{ + IsNonInteractiveWindowsMigration: true, + ConfigFilePath: absPath, + } + processors.StartProcessor = basicInfo.Processor + err = RunWizard(params) + + // Assert no error occurred + assert.NoError(t, err, "RunWizard returned an error") + + // Assert expected behaviour + callCount := processorMock.Calls + assert.Equal(t, 7, len(callCount), "Expected 7 calls to VerifyProcessor, got %d", len(callCount)) + + // Assert the resultant output file + outputPath, err := filepath.Abs("../../tool/processors/migration/windows/testData/output1.json") + assert.NoError(t, err, "Failed to get absolute path for output file") + + // Verify that the output file exists + _, err = os.Stat(outputPath) + assert.NoError(t, err, "Output file does not exist: %s", outputPath) + + expectedConfig, err := windows.ReadNewConfigFromPath(outputPath) + if err != nil { + t.Fatalf("Failed to read expected config: %v", err) + } + + actualConfigPath := util.ConfigFilePath() + t.Logf("Actual config path: %s", actualConfigPath) + + // Verify that the actual config file exists and is not empty + actualConfigInfo, err := os.Stat(actualConfigPath) + assert.NoError(t, err, "Actual config file does not exist: %s", actualConfigPath) + assert.NotEqual(t, 0, actualConfigInfo.Size(), "Actual config file is empty") + + actualConfig, err := windows.ReadNewConfigFromPath(actualConfigPath) + if err != nil { + t.Fatalf("Failed to read actual config: %v", err) + } + + assert.True(t, windows.AreTwoConfigurationsEqual(actualConfig, expectedConfig), + "The generated new config is incorrect, got: '%v', want: '%v'.", actualConfig, expectedConfig) +} diff --git a/translator/cmdutil/translator.go b/translator/cmdutil/translator.go new file mode 100644 index 0000000000..f8896f4c1a --- /dev/null +++ b/translator/cmdutil/translator.go @@ -0,0 +1,139 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package cmdutil + +import ( + "errors" + "fmt" + "log" + "os" + "os/user" + "path/filepath" + + "github.com/aws/amazon-cloudwatch-agent/cfg/commonconfig" + userutil "github.com/aws/amazon-cloudwatch-agent/internal/util/user" + "github.com/aws/amazon-cloudwatch-agent/translator" + "github.com/aws/amazon-cloudwatch-agent/translator/context" + "github.com/aws/amazon-cloudwatch-agent/translator/translate/otel/pipeline" + translatorUtil "github.com/aws/amazon-cloudwatch-agent/translator/util" +) + +const ( + exitErrorMessage = "Configuration validation first phase failed. Agent version: %v. Verify the JSON input is only using features supported by this version.\n" + exitSuccessMessage = "Configuration validation first phase succeeded" + version = "1.0" + envConfigFileName = "env-config.json" + yamlConfigFileName = "amazon-cloudwatch-agent.yaml" +) + +type ConfigTranslator struct { + ctx *context.Context +} + +func RunTranslator(flags map[string]*string) error { + ct, err := NewConfigTranslator( + *flags["os"], + *flags["input"], + *flags["input-dir"], + *flags["output"], + *flags["mode"], + *flags["config"], + *flags["multi-config"], + ) + if err != nil { + return err + } + return ct.Translate() +} + +func NewConfigTranslator(inputOs, inputJSONFile, inputJSONDir, inputTOMLFile, inputMode, inputConfig, multiConfig string) (*ConfigTranslator, error) { + + ct := ConfigTranslator{ + ctx: context.CurrentContext(), + } + + ct.ctx.SetOs(inputOs) + ct.ctx.SetInputJsonFilePath(inputJSONFile) + ct.ctx.SetInputJsonDirPath(inputJSONDir) + ct.ctx.SetMultiConfig(multiConfig) + ct.ctx.SetOutputTomlFilePath(inputTOMLFile) + + if inputConfig != "" { + f, err := os.Open(inputConfig) + if err != nil { + return nil, fmt.Errorf("failed to open common-config file %s with error: %v", inputConfig, err) + } + defer f.Close() + conf, err := commonconfig.Parse(f) + if err != nil { + return nil, fmt.Errorf("failed to parse common-config file %s with error: %v", inputConfig, err) + } + ct.ctx.SetCredentials(conf.CredentialsMap()) + ct.ctx.SetProxy(conf.ProxyMap()) + ct.ctx.SetSSL(conf.SSLMap()) + translatorUtil.LoadImdsRetries(conf.IMDS) + } + translatorUtil.SetProxyEnv(ct.ctx.Proxy()) + translatorUtil.SetSSLEnv(ct.ctx.SSL()) + + mode := translatorUtil.DetectAgentMode(inputMode) + ct.ctx.SetMode(mode) + ct.ctx.SetKubernetesMode(translatorUtil.DetectKubernetesMode(mode)) + + return &ct, nil +} + +func (ct *ConfigTranslator) Translate() error { + defer func() { + if r := recover(); r != nil { + if val, ok := r.(string); ok { + log.Println(val) + } + for _, errMessage := range translator.ErrorMessages { + log.Println(errMessage) + } + log.Printf(exitErrorMessage, version) + } + }() + + mergedJSONConfigMap, err := GenerateMergedJsonConfigMap(ct.ctx) + if err != nil { + return fmt.Errorf("failed to generate merged json config: %v", err) + } + + if !ct.ctx.RunInContainer() { + current, err := user.Current() + if err == nil && current.Name == "****" { + runAsUser, err := userutil.DetectRunAsUser(mergedJSONConfigMap) + if err != nil { + return fmt.Errorf("failed to detectRunAsUser") + } + VerifyCredentials(ct.ctx, runAsUser) + } + } + + tomlConfigPath := GetTomlConfigPath(ct.ctx.OutputTomlFilePath()) + tomlConfigDir := filepath.Dir(tomlConfigPath) + yamlConfigPath := filepath.Join(tomlConfigDir, yamlConfigFileName) + tomlConfig, err := TranslateJsonMapToTomlConfig(mergedJSONConfigMap) + if err != nil { + return fmt.Errorf("failed to generate TOML configuration validation content: %v", err) + } + yamlConfig, err := TranslateJsonMapToYamlConfig(mergedJSONConfigMap) + if err != nil && !errors.Is(err, pipeline.ErrNoPipelines) { + return fmt.Errorf("failed to generate YAML configuration validation content: %v", err) + } + if err = ConfigToTomlFile(tomlConfig, tomlConfigPath); err != nil { + return fmt.Errorf("failed to create the configuration TOML validation file: %v", err) + } + if err = ConfigToYamlFile(yamlConfig, yamlConfigPath); err != nil { + return fmt.Errorf("failed to create the configuration YAML validation file: %v", err) + } + log.Println(exitSuccessMessage) + + envConfigPath := filepath.Join(tomlConfigDir, envConfigFileName) + TranslateJsonMapToEnvConfigFile(mergedJSONConfigMap, envConfigPath) + + return nil +} diff --git a/translator/cmdutil/translatorutil_test.go b/translator/cmdutil/translatorutil_test.go index 014c85aaa9..4dbfa2c06e 100644 --- a/translator/cmdutil/translatorutil_test.go +++ b/translator/cmdutil/translatorutil_test.go @@ -7,11 +7,13 @@ import ( "encoding/json" "os" "path" + "regexp" "testing" "github.com/stretchr/testify/assert" "github.com/aws/amazon-cloudwatch-agent/cfg/envconfig" + "github.com/aws/amazon-cloudwatch-agent/translator/util" ) func TestTranslateJsonMapToEnvConfigFile(t *testing.T) { @@ -38,3 +40,195 @@ func TestTranslateJsonMapToEnvConfigFile(t *testing.T) { assert.Equal(t, expectedJson[envconfig.CWAGENT_LOG_LEVEL], actualJson[envconfig.CWAGENT_LOG_LEVEL]) assert.Equal(t, expectedJson[envconfig.AWS_SDK_LOG_LEVEL], actualJson[envconfig.AWS_SDK_LOG_LEVEL]) } + +func TestAgentConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validAgent.json", true, map[string]int{}) + expectedErrorMap := map[string]int{} + expectedErrorMap["invalid_type"] = 5 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidAgent.json", false, expectedErrorMap) +} + +func TestTracesConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validTrace.json", true, map[string]int{}) + expectedErrorMap := map[string]int{} + expectedErrorMap["array_min_properties"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidTrace.json", false, expectedErrorMap) +} + +func TestJMXConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validJMX.json", true, map[string]int{}) + expectedErrorMap := map[string]int{} + expectedErrorMap["additional_property_not_allowed"] = 1 + expectedErrorMap["number_any_of"] = 1 + expectedErrorMap["number_one_of"] = 1 + expectedErrorMap["required"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidJMX.json", false, expectedErrorMap) +} + +func TestOTLPMetricsConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validOTLPMetrics.json", true, map[string]int{}) + expectedErrorMap := map[string]int{} + expectedErrorMap["array_min_items"] = 1 + expectedErrorMap["number_one_of"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidOTLPMetrics.json", false, expectedErrorMap) +} + +func TestLogFilesConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validLogFiles.json", true, map[string]int{}) + expectedErrorMap := map[string]int{} + expectedErrorMap["array_min_items"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogFilesWithNoFileConfigured.json", false, expectedErrorMap) + expectedErrorMap1 := map[string]int{} + expectedErrorMap1["required"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogFilesWithMissingFilePath.json", false, expectedErrorMap1) + expectedErrorMap2 := map[string]int{} + expectedErrorMap2["unique"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogFilesWithDuplicateEntry.json", false, expectedErrorMap2) +} + +func TestLogWindowsEventConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validLogWindowsEvents.json", true, map[string]int{}) + expectedErrorMap := map[string]int{} + expectedErrorMap["number_not"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogWindowsEventsWithInvalidEventName.json", false, expectedErrorMap) + expectedErrorMap1 := map[string]int{} + expectedErrorMap1["required"] = 2 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogWindowsEventsWithMissingEventNameAndLevel.json", false, expectedErrorMap1) + expectedErrorMap2 := map[string]int{} + expectedErrorMap2["invalid_type"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogWindowsEventsWithInvalidEventLevelType.json", false, expectedErrorMap2) + expectedErrorMap3 := map[string]int{} + expectedErrorMap3["enum"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogWindowsEventsWithInvalidEventFormatType.json", false, expectedErrorMap3) +} + +func TestMetricsConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validLinuxMetrics.json", true, map[string]int{}) + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validWindowsMetrics.json", true, map[string]int{}) + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validMetricsWithAppSignals.json", true, map[string]int{}) + expectedErrorMap := map[string]int{} + expectedErrorMap["invalid_type"] = 2 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithInvalidAggregationDimensions.json", false, expectedErrorMap) + expectedErrorMap1 := map[string]int{} + expectedErrorMap1["array_min_properties"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithNoMetricsDefined.json", false, expectedErrorMap1) + expectedErrorMap2 := map[string]int{} + expectedErrorMap2["required"] = 1 + expectedErrorMap2["invalid_type"] = 2 + expectedErrorMap2["number_one_of"] = 2 + expectedErrorMap2["number_all_of"] = 3 + expectedErrorMap2["unique"] = 1 + expectedErrorMap2["number_gte"] = 1 + expectedErrorMap2["string_gte"] = 2 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithInvalidMeasurement.json", false, expectedErrorMap2) + expectedErrorMap3 := map[string]int{} + expectedErrorMap3["invalid_type"] = 2 + expectedErrorMap3["number_all_of"] = 2 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithInvalidAppendDimensions.json", false, expectedErrorMap3) + expectedErrorMap4 := map[string]int{} + expectedErrorMap4["enum"] = 1 + expectedErrorMap4["array_max_items"] = 1 + expectedErrorMap4["invalid_type"] = 1 + expectedErrorMap4["number_all_of"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithinvalidMetricsCollected.json", false, expectedErrorMap4) + expectedErrorMap5 := map[string]int{} + expectedErrorMap5["additional_property_not_allowed"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithAdditionalProperties.json", false, expectedErrorMap5) + expectedErrorMap6 := map[string]int{} + expectedErrorMap6["required"] = 1 + expectedErrorMap6["invalid_type"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsWithInvalidMetrics_Collected.json", false, expectedErrorMap6) +} + +func TestProcstatConfig(t *testing.T) { + expectedErrorMap := map[string]int{} + expectedErrorMap["invalid_type"] = 1 + expectedErrorMap["number_all_of"] = 1 + expectedErrorMap["number_any_of"] = 1 + expectedErrorMap["required"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidProcstatMeasurement.json", false, expectedErrorMap) + + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validProcstatConfig.json", true, map[string]int{}) +} + +func TestEthtoolConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validEthtoolConfig.json", true, map[string]int{}) +} + +func TestNvidiaGpuConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validNvidiaGpuConfig.json", true, map[string]int{}) +} + +func TestValidLogFilterConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validLogFilesWithFilters.json", true, map[string]int{}) +} + +func TestInvalidLogFilterConfig(t *testing.T) { + expectedErrorMap := map[string]int{ + "additional_property_not_allowed": 1, + "enum": 1, + } + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidLogFilesWithFilters.json", false, expectedErrorMap) +} + +func TestMetricsDestinationsConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validMetricsDestinations.json", true, map[string]int{}) + expectedErrorMap := map[string]int{} + expectedErrorMap["required"] = 1 + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/invalidMetricsDestinations.json", false, expectedErrorMap) +} +func TestContainerInsightsJmxConfig(t *testing.T) { + checkIfSchemaValidateAsExpected(t, "../../translator/config/sampleSchema/validContainerInsightsJmx.json", true, map[string]int{}) +} + +// Validate all sampleConfig files schema +func TestSampleConfigSchema(t *testing.T) { + if files, err := os.ReadDir("../../translator/tocwconfig/sampleConfig/"); err == nil { + re := regexp.MustCompile(".json") + for _, file := range files { + if re.MatchString(file.Name()) { + t.Logf("Validating ../../translator/tocwconfig/sampleConfig/%s\n", file.Name()) + checkIfSchemaValidateAsExpected(t, "../../translator/tocwconfig/sampleConfig/"+file.Name(), true, map[string]int{}) + t.Logf("Validated ../../translator/tocwconfig/sampleConfig/%s\n", file.Name()) + } + } + } else { + panic(err) + } +} + +func checkIfSchemaValidateAsExpected(t *testing.T, jsonInputPath string, shouldSuccess bool, expectedErrorMap map[string]int) { + actualErrorMap := make(map[string]int) + + jsonInputMap, err := util.GetJsonMapFromFile(jsonInputPath) + if err != nil { + t.Fatalf("Failed to get json map from %v with error: %v", jsonInputPath, err) + } + + result, err := RunSchemaValidation(jsonInputMap) + if err != nil { + t.Fatalf("Failed to run schema validation: %v", err) + } + + if result.Valid() { + assert.True(t, shouldSuccess, "It should fail the schemaValidation!") + } else { + errorDetails := result.Errors() + for _, errorDetail := range errorDetails { + t.Logf("String: %v \n", errorDetail.String()) + t.Logf("Context: %v \n", errorDetail.Context().String()) + t.Logf("Description: %v \n", errorDetail.Description()) + t.Logf("Details: %v \n", errorDetail.Details()) + t.Logf("Field: %v \n", errorDetail.Field()) + t.Logf("Type: %v \n", errorDetail.Type()) + t.Logf("Value: %v \n", errorDetail.Value()) + if _, ok := actualErrorMap[errorDetail.Type()]; ok { + actualErrorMap[errorDetail.Type()]++ + } else { + actualErrorMap[errorDetail.Type()] = 1 + } + } + assert.Equal(t, expectedErrorMap, actualErrorMap, "Unexpected error set!") + assert.False(t, shouldSuccess, "It should pass the schemaValidation!") + } +} diff --git a/translator/flags/flags.go b/translator/flags/flags.go new file mode 100644 index 0000000000..b82a3debcd --- /dev/null +++ b/translator/flags/flags.go @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package flags + +import "github.com/aws/amazon-cloudwatch-agent/tool/cmdwrapper" + +const TranslatorCommand = "config-translator" + +var TranslatorFlags = map[string]cmdwrapper.Flag{ + "os": {DefaultValue: "", Description: "Please provide the os preference, valid value: windows/linux."}, + "input": {DefaultValue: "", Description: "Please provide the path of input agent json config file"}, + "input-dir": {DefaultValue: "", Description: "Please provide the path of input agent json config directory."}, + "output": {DefaultValue: "", Description: "Please provide the path of the output CWAgent config file"}, + "mode": {DefaultValue: "ec2", Description: "Please provide the mode, i.e. ec2, onPremise, onPrem, auto"}, + "config": {DefaultValue: "", Description: "Please provide the common-config file"}, + "multi-config": {DefaultValue: "remove", Description: "valid values: default, append, remove"}, +}