github.com/Axway/agent-sdk@v1.1.101/pkg/cmd/root.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"net/url"
     7  	"os"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/Axway/agent-sdk/pkg/agent"
    12  	"github.com/Axway/agent-sdk/pkg/apic"
    13  	"github.com/Axway/agent-sdk/pkg/cmd/agentsync"
    14  	"github.com/Axway/agent-sdk/pkg/cmd/properties"
    15  	"github.com/Axway/agent-sdk/pkg/cmd/properties/resolver"
    16  	"github.com/Axway/agent-sdk/pkg/config"
    17  	"github.com/Axway/agent-sdk/pkg/jobs"
    18  	"github.com/Axway/agent-sdk/pkg/util"
    19  	"github.com/Axway/agent-sdk/pkg/util/errors"
    20  	hc "github.com/Axway/agent-sdk/pkg/util/healthcheck"
    21  	"github.com/Axway/agent-sdk/pkg/util/log"
    22  
    23  	"github.com/fsnotify/fsnotify"
    24  	"github.com/spf13/cobra"
    25  
    26  	"github.com/spf13/viper"
    27  )
    28  
    29  // Constants for cmd flags
    30  const (
    31  	pathConfigFlag         = "pathConfig"
    32  	beatsPathConfigFlag    = "path.config"
    33  	EnvFileFlag            = "envFile"
    34  	EnvFileFlagDescription = "Path of the file with environment variables to override configuration"
    35  	cpuprofile             = "cpuprofile"
    36  	memprofile             = "memprofile"
    37  	httpprofile            = "httpprofile"
    38  )
    39  
    40  // CommandHandler - Root command execution handler
    41  type CommandHandler func() error
    42  
    43  // InitConfigHandler - Handler to be invoked on config initialization
    44  type InitConfigHandler func(centralConfig config.CentralConfig) (interface{}, error)
    45  
    46  // AgentRootCmd - Root Command for the Agents
    47  type AgentRootCmd interface {
    48  	RootCmd() *cobra.Command
    49  	Execute() error
    50  
    51  	// Get the agentType
    52  	GetAgentType() config.AgentType
    53  	AddCommand(*cobra.Command)
    54  
    55  	GetProperties() properties.Properties
    56  }
    57  
    58  // agentRootCommand - Represents the agent root command
    59  type agentRootCommand struct {
    60  	agentName         string
    61  	rootCmd           *cobra.Command
    62  	commandHandler    CommandHandler
    63  	initConfigHandler InitConfigHandler
    64  	agentType         config.AgentType
    65  	props             properties.Properties
    66  	statusCfg         config.StatusConfig
    67  	agentFeaturesCfg  config.AgentFeaturesConfig
    68  	centralCfg        config.CentralConfig
    69  	agentCfg          interface{}
    70  	secretResolver    resolver.SecretResolver
    71  	initialized       bool
    72  	memprofile        string
    73  	cpuprofile        string
    74  	httpprofile       bool
    75  }
    76  
    77  func init() {
    78  	config.AgentTypeName = BuildAgentName
    79  	config.AgentVersion = BuildVersion + "-" + BuildCommitSha
    80  	config.AgentDataPlaneType = apic.Unidentified.String()
    81  	if BuildDataPlaneType != "" {
    82  		config.AgentDataPlaneType = BuildDataPlaneType
    83  	}
    84  
    85  	config.SDKVersion = SDKBuildVersion
    86  	// initalize the global Source used by rand.Intn() and other functions of the rand package using rand.Seed().
    87  	rand.Seed(time.Now().UnixNano())
    88  }
    89  
    90  func buildCmdVersion(desc string) string {
    91  	return fmt.Sprintf("- %s", buildAgentInfo(desc))
    92  }
    93  
    94  func buildAgentInfo(desc string) string {
    95  	return fmt.Sprintf("%s version %s-%s, Amplify Agents SDK version %s", desc, BuildVersion, BuildCommitSha, SDKBuildVersion)
    96  }
    97  
    98  // NewRootCmd - Creates a new Agent Root Command
    99  func NewRootCmd(exeName, desc string, initConfigHandler InitConfigHandler, commandHandler CommandHandler, agentType config.AgentType) AgentRootCmd {
   100  	c := &agentRootCommand{
   101  		agentName:         exeName,
   102  		commandHandler:    commandHandler,
   103  		initConfigHandler: initConfigHandler,
   104  		agentType:         agentType,
   105  		secretResolver:    resolver.NewSecretResolver(),
   106  		initialized:       false,
   107  	}
   108  
   109  	// use the description from the build if available
   110  	if BuildAgentDescription != "" {
   111  		desc = BuildAgentDescription
   112  	}
   113  
   114  	c.rootCmd = &cobra.Command{
   115  		Use:     c.agentName,
   116  		Short:   desc,
   117  		Version: buildCmdVersion(desc),
   118  		RunE:    c.run,
   119  		PreRunE: c.initialize,
   120  	}
   121  
   122  	c.props = properties.NewPropertiesWithSecretResolver(c.rootCmd, c.secretResolver)
   123  	c.addBaseProps(agentType)
   124  	config.AddLogConfigProperties(c.props, fmt.Sprintf("%s.log", exeName))
   125  	config.AddMetricLogConfigProperties(c.props, agentType)
   126  	config.AddUsageConfigProperties(c.props, agentType)
   127  	agentsync.AddSyncConfigProperties(c.props)
   128  	config.AddCentralConfigProperties(c.props, agentType)
   129  	config.AddStatusConfigProperties(c.props)
   130  	config.AddAgentFeaturesConfigProperties(c.props)
   131  
   132  	hc.SetNameAndVersion(exeName, c.rootCmd.Version)
   133  
   134  	// Call the config add props
   135  	return c
   136  }
   137  
   138  // NewCmd - Creates a new Agent Root Command using existing cmd
   139  func NewCmd(rootCmd *cobra.Command, exeName, desc string, initConfigHandler InitConfigHandler, commandHandler CommandHandler, agentType config.AgentType) AgentRootCmd {
   140  	c := &agentRootCommand{
   141  		agentName:         exeName,
   142  		commandHandler:    commandHandler,
   143  		initConfigHandler: initConfigHandler,
   144  		agentType:         agentType,
   145  		secretResolver:    resolver.NewSecretResolver(),
   146  		initialized:       false,
   147  	}
   148  
   149  	// use the description from the build if available
   150  	if BuildAgentDescription != "" {
   151  		desc = BuildAgentDescription
   152  	}
   153  
   154  	c.rootCmd = rootCmd
   155  	c.rootCmd.Use = c.agentName
   156  	c.rootCmd.Short = desc
   157  	c.rootCmd.Version = buildCmdVersion(desc)
   158  	c.rootCmd.RunE = c.run
   159  	c.rootCmd.PreRunE = c.initialize
   160  
   161  	c.props = properties.NewPropertiesWithSecretResolver(c.rootCmd, c.secretResolver)
   162  	if agentType == config.TraceabilityAgent {
   163  		properties.SetAliasKeyPrefix(c.agentName)
   164  	}
   165  
   166  	c.addBaseProps(agentType)
   167  	config.AddLogConfigProperties(c.props, fmt.Sprintf("%s.log", exeName))
   168  	agentsync.AddSyncConfigProperties(c.props)
   169  	config.AddCentralConfigProperties(c.props, agentType)
   170  	config.AddStatusConfigProperties(c.props)
   171  	config.AddAgentFeaturesConfigProperties(c.props)
   172  
   173  	hc.SetNameAndVersion(exeName, c.rootCmd.Version)
   174  
   175  	removeBeatSubCommands(c.rootCmd)
   176  	// Call the config add props
   177  	return c
   178  }
   179  
   180  func removeBeatSubCommands(rootCmd *cobra.Command) {
   181  	removeBeatSubCommand(rootCmd, "export")
   182  	removeBeatSubCommand(rootCmd, "keystore")
   183  	removeBeatSubCommand(rootCmd, "run")
   184  	removeBeatSubCommand(rootCmd, "setup")
   185  	removeBeatSubCommand(rootCmd, "test")
   186  	removeBeatSubCommand(rootCmd, "version")
   187  }
   188  
   189  func removeBeatSubCommand(rootCmd *cobra.Command, subCmdName string) {
   190  	subCmd, _, err := rootCmd.Find([]string{subCmdName})
   191  	if err == nil {
   192  		rootCmd.RemoveCommand(subCmd)
   193  	}
   194  }
   195  
   196  // Add the command line properties for the logger and path config
   197  func (c *agentRootCommand) addBaseProps(agentType config.AgentType) {
   198  	c.props.AddStringPersistentFlag(pathConfigFlag, ".", "Path to the directory containing the YAML configuration file for the agent")
   199  	c.props.AddStringPersistentFlag(EnvFileFlag, "", EnvFileFlagDescription)
   200  	if agentType == config.DiscoveryAgent {
   201  		c.props.AddStringProperty(cpuprofile, "", "write cpu profile to `file`")
   202  		c.props.AddStringProperty(memprofile, "", "write memory profile to `file`")
   203  		c.props.AddBoolProperty(httpprofile, false, "set to setup the http profiling endpoints")
   204  	}
   205  }
   206  
   207  func (c *agentRootCommand) initialize(cmd *cobra.Command, args []string) error {
   208  	_, envFile := c.props.StringFlagValue(EnvFileFlag)
   209  	err := util.LoadEnvFromFile(envFile)
   210  	if err != nil {
   211  		return errors.Wrap(config.ErrEnvConfigOverride, err.Error())
   212  	}
   213  
   214  	_, agentConfigFilePath := c.props.StringFlagValue(pathConfigFlag)
   215  	_, beatsConfigFilePath := c.props.StringFlagValue(beatsPathConfigFlag)
   216  	if c.agentType == config.DiscoveryAgent {
   217  		_, c.cpuprofile = c.props.StringFlagValue(cpuprofile)
   218  		_, c.memprofile = c.props.StringFlagValue(memprofile)
   219  		c.httpprofile = c.props.BoolFlagValue(httpprofile)
   220  	}
   221  
   222  	// If the Agent pathConfig value is set and the beats path.config is not then use the pathConfig value for both
   223  	if beatsConfigFilePath == "" && agentConfigFilePath != "" {
   224  		c.props.SetStringFlagValue(beatsPathConfigFlag, agentConfigFilePath)
   225  		_, beatsConfigFilePath = c.props.StringFlagValue(beatsPathConfigFlag)
   226  	}
   227  
   228  	viper.SetConfigName(c.agentName)
   229  	// viper.SetConfigType("yaml")  //Comment out since yaml, yml is a support extension already.  We need an updated story to take into account the other supported extensions
   230  
   231  	// Add both the agent pathConfig and beats path.config paths to the config path array
   232  	viper.AddConfigPath(agentConfigFilePath)
   233  	viper.AddConfigPath(beatsConfigFilePath)
   234  	viper.AddConfigPath(".")
   235  	viper.SetTypeByDefaultValue(true)
   236  	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
   237  	viper.AutomaticEnv()
   238  	err = viper.ReadInConfig()
   239  	if err != nil {
   240  		if envFile == "" {
   241  			return err
   242  		} else if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
   243  			return err
   244  		}
   245  	}
   246  
   247  	viper.WatchConfig()
   248  	viper.OnConfigChange(func(e fsnotify.Event) {
   249  		log.Debugf("Config file changed : %s", e.Name)
   250  		c.onConfigChange()
   251  	})
   252  
   253  	c.checkStatusFlag()
   254  	agentsync.SetSyncMode(c.GetProperties())
   255  	return nil
   256  }
   257  
   258  func (c *agentRootCommand) checkStatusFlag() {
   259  	statusPort := c.props.IntPropertyValue("status.port")
   260  	if c.props.BoolFlagValue("status") {
   261  		urlObj := url.URL{
   262  			Scheme: "http",
   263  			Host:   fmt.Sprintf("localhost:%d", statusPort),
   264  			Path:   "status",
   265  		}
   266  		statusOut, err := hc.GetHealthcheckOutput(urlObj.String())
   267  		if statusOut != "" {
   268  			fmt.Println(statusOut)
   269  		}
   270  
   271  		if err != nil {
   272  			fmt.Println("Error in getting status : " + err.Error())
   273  			os.Exit(1)
   274  		}
   275  		os.Exit(0)
   276  	}
   277  }
   278  
   279  func (c *agentRootCommand) onConfigChange() {
   280  	c.initConfig()
   281  	agentConfigChangeHandler := agent.GetConfigChangeHandler()
   282  	if agentConfigChangeHandler != nil {
   283  		agentConfigChangeHandler()
   284  	}
   285  }
   286  
   287  // initConfig - Initializes the central config and invokes initConfig handler
   288  // to initialize the agent config. Performs validation on returned agent config
   289  func (c *agentRootCommand) initConfig() error {
   290  	// Clean the secret map on config change
   291  	c.secretResolver.ResetResolver()
   292  
   293  	_, err := config.ParseAndSetupLogConfig(c.GetProperties(), c.agentType)
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	c.statusCfg, _ = config.ParseStatusConfig(c.GetProperties())
   299  	err = c.statusCfg.ValidateCfg()
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	// Init Agent Features Config
   305  	c.agentFeaturesCfg, err = config.ParseAgentFeaturesConfig(c.GetProperties())
   306  	if err != nil {
   307  		return err
   308  	}
   309  
   310  	// Init Central Config
   311  	c.centralCfg, err = config.ParseCentralConfig(c.GetProperties(), c.GetAgentType())
   312  	if err != nil {
   313  		return err
   314  	}
   315  
   316  	// must set the hc config now, because the healthchecker loop starts in agent.Initialize
   317  	hc.SetStatusConfig(c.statusCfg)
   318  
   319  	err = agent.InitializeWithAgentFeatures(c.centralCfg, c.agentFeaturesCfg)
   320  	if err != nil {
   321  		return err
   322  	}
   323  	agent.InitializeProfiling(c.cpuprofile, c.memprofile)
   324  
   325  	jobs.UpdateDurations(c.statusCfg.GetHealthCheckInterval(), c.centralCfg.GetJobExecutionTimeout())
   326  
   327  	// Initialize Agent Config
   328  	c.agentCfg, err = c.initConfigHandler(c.centralCfg)
   329  	if err != nil {
   330  		return err
   331  	}
   332  
   333  	if c.agentCfg != nil {
   334  		err := agent.ApplyResourceToConfig(c.agentCfg)
   335  		if err != nil {
   336  			return err
   337  		}
   338  
   339  		// Validate Agent Config
   340  		err = config.ValidateConfig(c.agentCfg)
   341  		if err != nil {
   342  			return err
   343  		}
   344  	}
   345  
   346  	if !c.initialized {
   347  		err = c.finishInit()
   348  		if err != nil {
   349  			return err
   350  		}
   351  	}
   352  	c.initialized = true
   353  	return nil
   354  }
   355  
   356  func (c *agentRootCommand) finishInit() error {
   357  	if util.IsNotTest() && c.agentFeaturesCfg.ConnectionToCentralEnabled() && !c.centralCfg.GetUsageReportingConfig().IsOfflineMode() {
   358  		eventSync, err := agent.NewEventSync()
   359  		if err != nil {
   360  			return errors.Wrap(errors.ErrInitServicesNotReady, err.Error())
   361  		}
   362  
   363  		if err := eventSync.SyncCache(); err != nil {
   364  			return errors.Wrap(errors.ErrInitServicesNotReady, err.Error())
   365  		}
   366  		// set the rebuild function in the agent resource manager
   367  		agent.GetAgentResourceManager().SetRebuildCacheFunc(eventSync)
   368  
   369  	}
   370  
   371  	// Start the initial and recurring version check jobs
   372  	startVersionCheckJobs(c.centralCfg, c.agentFeaturesCfg)
   373  
   374  	if util.IsNotTest() {
   375  		healthCheckServer := hc.NewServer(c.httpprofile)
   376  		healthCheckServer.HandleRequests()
   377  	}
   378  
   379  	return nil
   380  }
   381  
   382  // run - Executes the agent command
   383  func (c *agentRootCommand) run(cmd *cobra.Command, args []string) (err error) {
   384  	err = c.initConfig()
   385  	statusText := ""
   386  	if err == nil {
   387  		// Register resource change handler to re-initialize config on resource change
   388  		// This should trigger config init and applyresourcechange handlers
   389  		agent.OnAgentResourceChange(c.onConfigChange)
   390  
   391  		// Check the sync flag
   392  		exitcode := agentsync.CheckSyncFlag()
   393  		if exitcode > -1 {
   394  			os.Exit(exitcode)
   395  		}
   396  
   397  		log.Infof("Starting %s", buildAgentInfo(c.rootCmd.Short))
   398  		if c.commandHandler != nil {
   399  			// Setup logp to use beats logger.
   400  			// Setting up late here as log entries for agent/command initialization are not logged
   401  			// as the beats logger is initialized only when the beat instance is created.
   402  			if c.agentType == config.TraceabilityAgent {
   403  				properties.SetAliasKeyPrefix(c.agentName)
   404  				log.SetIsLogP()
   405  			}
   406  
   407  			c.healthCheckTicker()
   408  
   409  			if util.IsNotTest() && c.agentFeaturesCfg.AgentStatusUpdatesEnabled() && !c.centralCfg.GetUsageReportingConfig().IsOfflineMode() {
   410  				agent.StartAgentStatusUpdate()
   411  			}
   412  
   413  			err = c.commandHandler()
   414  			if err != nil {
   415  				log.Error(err.Error())
   416  				statusText = err.Error()
   417  			}
   418  		}
   419  	} else {
   420  		statusText = err.Error()
   421  	}
   422  	status := agent.AgentStopped
   423  	if statusText != "" {
   424  		status = agent.AgentFailed
   425  	}
   426  	agent.UpdateStatusWithPrevious(status, agent.AgentRunning, statusText)
   427  	return
   428  }
   429  
   430  // Run health check ticker for every 5 seconds
   431  // If after 5 minutes, the health checker still returns HC status !OK, exit the agent.  Otherwise, return true and continue processing
   432  func (c *agentRootCommand) healthCheckTicker() {
   433  	log.Trace("run health checker ticker to check health status on RunChecks")
   434  	ticker := time.NewTicker(5 * time.Second)
   435  	tickerTimeout := time.NewTicker(5 * time.Minute)
   436  
   437  	defer ticker.Stop()
   438  	defer tickerTimeout.Stop()
   439  
   440  	for {
   441  		select {
   442  		case <-tickerTimeout.C:
   443  			log.Error("healthcheck run checks failing. Stopping agent - Check docs.axway.com for more info on the reported error code")
   444  			agent.UpdateStatus(agent.AgentFailed, "healthchecks on startup failed")
   445  			os.Exit(0)
   446  		case <-ticker.C:
   447  			status := hc.RunChecks()
   448  			if status == hc.OK {
   449  				log.Trace("healthcheck on startup is OK. Continue processing")
   450  				return
   451  			} else {
   452  				log.Warn("healthchecks on startup are still processing")
   453  			}
   454  		}
   455  	}
   456  }
   457  
   458  func (c *agentRootCommand) RootCmd() *cobra.Command {
   459  	return c.rootCmd
   460  }
   461  
   462  func (c *agentRootCommand) Execute() error {
   463  	return c.rootCmd.Execute()
   464  }
   465  
   466  func (c *agentRootCommand) GetAgentType() config.AgentType {
   467  	return c.agentType
   468  }
   469  
   470  func (c *agentRootCommand) GetProperties() properties.Properties {
   471  	return c.props
   472  }
   473  
   474  func (c *agentRootCommand) AddCommand(cmd *cobra.Command) {
   475  	c.rootCmd.AddCommand(cmd)
   476  }