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 65213aa44a..24a937a335 100644 --- a/cmd/amazon-cloudwatch-agent/amazon-cloudwatch-agent.go +++ b/cmd/amazon-cloudwatch-agent/amazon-cloudwatch-agent.go @@ -21,7 +21,6 @@ import ( "syscall" "time" - "github.com/aws/amazon-cloudwatch-agent/translator/cmdutil" "github.com/influxdata/telegraf/agent" "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/logger" @@ -42,13 +41,19 @@ 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/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" ) @@ -96,15 +101,10 @@ 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") -// config-translator -var fConfigTranslator = flag.Bool("config-translator", false, "run in config-translator mode") -var fTranslatorOs = flag.String("ct-os", "", "Please provide the os preference, valid value: windows/linux.") -var fTranslatorInput = flag.String("ct-input", "", "Please provide the path of input agent json config file") -var fTranslatorInputDir = flag.String("ct-input-dir", "", "Please provide the path of input agent json config directory.") -var fTranslatorOutput = flag.String("ct-output", "", "Please provide the path of the output CWAgent config file") -var fTranslatorMode = flag.String("ct-mode", "ec2", "Please provide the mode, i.e. ec2, onPremise, onPrem, auto") -var fTranslatorConfig = flag.String("ct-config", "", "Please provide the common-config file") -var fTranslatorMultiConfig = flag.String("ct-multi-config", "remove", "valid values: default, append, remove") +// 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{} @@ -496,6 +496,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) @@ -619,13 +623,21 @@ func main() { } return case *fConfigTranslator: - ct, err := cmdutil.NewConfigTranslator(*fTranslatorOs, *fTranslatorInput, *fTranslatorInputDir, *fTranslatorOutput, *fTranslatorMode, *fTranslatorConfig, *fTranslatorMultiConfig) + err := cmdutil.RunTranslator(translatorFlags) if err != nil { log.Fatalf("E! Failed to initialize config translator: %v", err) } - err = ct.Translate() + 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 translate config: %v", err) + log.Fatalf("E! Failed to run config wizard: %v", err) } return } 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 fd9d7f7791..2426888c92 100644 --- a/cmd/config-translator/translator.go +++ b/cmd/config-translator/translator.go @@ -1,61 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + package main import ( "flag" - "fmt" "log" "os" - "os/exec" - "github.com/aws/amazon-cloudwatch-agent/tool/paths" + "github.com/aws/amazon-cloudwatch-agent/tool/cmdwrapper" + "github.com/aws/amazon-cloudwatch-agent/translator/flags" ) -type flagDef struct { - name string - value *string - defaultVal string - description string -} - func main() { log.Printf("Starting config-translator, this will map back to a call to amazon-cloudwatch-agent") - flags := []flagDef{ - {"os", nil, "", "Please provide the os preference, valid value: windows/linux."}, - {"input", nil, "", "Please provide the path of input agent json config file"}, - {"input-dir", nil, "", "Please provide the path of input agent json config directory."}, - {"output", nil, "", "Please provide the path of the output CWAgent config file"}, - {"mode", nil, "ec2", "Please provide the mode, i.e. ec2, onPremise, onPrem, auto"}, - {"config", nil, "", "Please provide the common-config file"}, - {"multi-config", nil, "remove", "valid values: default, append, remove"}, - } - - for i := range flags { - flags[i].value = flag.String(flags[i].name, flags[i].defaultVal, flags[i].description) - } + translatorFlags := cmdwrapper.AddFlags("", flags.TranslatorFlags) flag.Parse() - args := []string{"-config-translator"} - for _, f := range flags { - if *f.value != "" { - // prefix ct so we do not accidentally overlap with other agent flags - args = append(args, fmt.Sprintf("-ct-%s", f.name), *f.value) - } - } - - log.Printf("Executing %s with arguments: %v", paths.AgentBinaryPath, args) - - cmd := exec.Command(paths.AgentBinaryPath, args...) - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() + err := cmdwrapper.ExecuteAgentCommand(flags.TranslatorCommand, translatorFlags) if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - 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) os.Exit(1) } } diff --git a/tool/cmdwrapper/cmdwrapper.go b/tool/cmdwrapper/cmdwrapper.go new file mode 100644 index 0000000000..b547b33721 --- /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 { + Name string + 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 := flagConfig.Name + 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 + + 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..ebfa0c628e --- /dev/null +++ b/tool/cmdwrapper/cmdwrapper_test.go @@ -0,0 +1,136 @@ +// 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": { + Name: "test", + DefaultValue: "default", + Description: "test description", + }, + }, + want: map[string]string{ + "test": "default", + }, + }, + { + name: "with prefix", + prefix: "prefix", + flagConfigs: map[string]Flag{ + "test": { + Name: "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..ddc711a872 --- /dev/null +++ b/tool/downloader/downloader.go @@ -0,0 +1,220 @@ +// 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 = ":" + + exitErrorMessage = "Fail to fetch the config!" +) + +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 +} + +func RunDownloaderFromFlags(flags map[string]*string) error { + return RunDownloader( + *flags["mode"], + *flags["download-source"], + *flags["output-dir"], + *flags["config"], + *flags["multi-config"], + ) +} + +/** + * multi-config: + * default, append: download config to the dir and append .tmp suffix + * remove: remove the config from the dir + */ +func RunDownloader( + mode string, + downloadLocation string, + outputDir string, + inputConfig string, + multiConfig string, +) error { + // Initialize common config + 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()) + + // Validate region + 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") + } + + // Clean up output directory + err := 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 + } + fmt.Printf("Sub dir %v will be ignored.", path) + return filepath.SkipDir + } + if filepath.Ext(path) == constants.FileSuffixTmp { + return os.Remove(path) + } + return nil + }) + if err != nil { + return err + } + + // Parse download location + locationArray := strings.SplitN(downloadLocation, locationSeparator, 2) + if locationArray == nil || len(locationArray) < 2 && downloadLocation != locationDefault { + return fmt.Errorf("downloadLocation %s is malformed", downloadLocation) + } + + // Process configuration based on location type + 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 err + } + + // Handle configuration based on multiConfig setting + if multiConfig == "remove" { + outputPath := filepath.Join(outputDir, outputFilePath) + if err := os.Remove(outputPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove file %s: %v", outputPath, err) + } + } else { + outputPath := filepath.Join(outputDir, outputFilePath+constants.FileSuffixTmp) + if err := os.WriteFile(outputPath, []byte(config), 0644); err != nil { + return fmt.Errorf("failed to write to file %s: %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) { + 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 +} diff --git a/tool/downloader/flags/flags.go b/tool/downloader/flags/flags.go new file mode 100644 index 0000000000..cb55c5eed0 --- /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": {"mode", "ec2", "Please provide the mode, i.e. ec2, onPremise, onPrem, auto"}, + "download-source": {"download-source", "", "Download source. Example: \"ssm:my-parameter-store-name\" for an EC2 SSM Parameter Store Name holding your CloudWatch Agent configuration."}, + "output-dir": {"output-dir", "", "Path of output json config directory."}, + "config": {"config", "", "Please provide the common-config file"}, + "multi-config": {"multi-config", "default", "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..8eb6728af0 --- /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": {"isNonInteractiveWindowsMigration", "false", "If true, it will use command line args to bypass the wizard. Default value is false."}, + "is-non-interactive-linux-migration": {"isNonInteractiveLinuxMigration", "false", "If true, it will do the linux config migration. Default value is false."}, + "traces-only": {"tracesOnly", "false", "If true, only trace configuration will be generated"}, + "use-parameter-store": {"useParameterStore", "false", "If true, it will use the parameter store for the migrated config storage."}, + "non-interactive-xray-migration": {"nonInteractiveXrayMigration", "false", "If true, then this is part of non Interactive xray migration tool."}, + "config-file-path": {"configFilePath", "", fmt.Sprintf("The path of the old config file. Default is %s on Windows or %s on Linux", DefaultFilePathWindowsConfiguration, DefaultFilePathLinuxConfiguration)}, + "config-output-path": {"configOutputPath", "", "Specifies where to write the configuration file generated by the wizard"}, + "parameter-store-name": {"parameterStoreName", "", "The parameter store name. Default is AmazonCloudWatch-windows"}, + "parameter-store-region": {"parameterStoreRegion", "", "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..b8e8600785 --- /dev/null +++ b/tool/wizard/wizard.go @@ -0,0 +1,144 @@ +// 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{}) (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 + } + 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(processor 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 index e7789c9126..27d28dd9db 100644 --- a/translator/cmdutil/translator.go +++ b/translator/cmdutil/translator.go @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + package cmdutil import ( @@ -11,7 +14,6 @@ import ( "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" @@ -29,6 +31,22 @@ 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{ diff --git a/translator/cmdutil/translatorutil_test.go b/translator/cmdutil/translatorutil_test.go index 4b1c9f1b9e..0fc4e22ea5 100644 --- a/translator/cmdutil/translatorutil_test.go +++ b/translator/cmdutil/translatorutil_test.go @@ -10,10 +10,10 @@ import ( "regexp" "testing" - "github.com/aws/amazon-cloudwatch-agent/translator/util" "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) { diff --git a/translator/flags/flags.go b/translator/flags/flags.go new file mode 100644 index 0000000000..2313f742cc --- /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": {"os", "", "Please provide the os preference, valid value: windows/linux."}, + "input": {"input", "", "Please provide the path of input agent json config file"}, + "input-dir": {"input-dir", "", "Please provide the path of input agent json config directory."}, + "output": {"output", "", "Please provide the path of the output CWAgent config file"}, + "mode": {"mode", "ec2", "Please provide the mode, i.e. ec2, onPremise, onPrem, auto"}, + "config": {"config", "", "Please provide the common-config file"}, + "multi-config": {"multi-config", "remove", "valid values: default, append, remove"}, +}