github.com/bshelton229/agent@v3.5.4+incompatible/agent/agent_pool.go (about)

     1  package agent
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"runtime"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/buildkite/agent/api"
    13  	"github.com/buildkite/agent/logger"
    14  	"github.com/buildkite/agent/retry"
    15  	"github.com/buildkite/agent/signalwatcher"
    16  	"github.com/buildkite/agent/system"
    17  	"github.com/denisbrodbeck/machineid"
    18  )
    19  
    20  type AgentPool struct {
    21  	APIClient             *api.Client
    22  	Token                 string
    23  	ConfigFilePath        string
    24  	Name                  string
    25  	Priority              string
    26  	Tags                  []string
    27  	TagsFromEC2           bool
    28  	TagsFromEC2Tags       bool
    29  	TagsFromGCP           bool
    30  	TagsFromHost          bool
    31  	WaitForEC2TagsTimeout time.Duration
    32  	Endpoint              string
    33  	AgentConfiguration    *AgentConfiguration
    34  
    35  	interruptCount int
    36  	signalLock     sync.Mutex
    37  }
    38  
    39  func (r *AgentPool) Start() error {
    40  	// Show the welcome banner and config options used
    41  	r.ShowBanner()
    42  
    43  	// Create the agent registration API Client
    44  	r.APIClient = APIClient{Endpoint: r.Endpoint, Token: r.Token}.Create()
    45  
    46  	// Create the agent template. We use pass this template to the register
    47  	// call, at which point we get back a real agent.
    48  	template := r.CreateAgentTemplate()
    49  
    50  	logger.Info("Registering agent with Buildkite...")
    51  
    52  	// Register the agent
    53  	registered, err := r.RegisterAgent(template)
    54  	if err != nil {
    55  		logger.Fatal("%s", err)
    56  	}
    57  
    58  	logger.Info("Successfully registered agent \"%s\" with tags [%s]", registered.Name,
    59  		strings.Join(registered.Tags, ", "))
    60  
    61  	logger.Debug("Ping interval: %ds", registered.PingInterval)
    62  	logger.Debug("Job status interval: %ds", registered.JobStatusInterval)
    63  	logger.Debug("Heartbeat interval: %ds", registered.HearbeatInterval)
    64  
    65  	// Now that we have a registered agent, we can connect it to the API,
    66  	// and start running jobs.
    67  	worker := AgentWorker{Agent: registered, AgentConfiguration: r.AgentConfiguration, Endpoint: r.Endpoint}.Create()
    68  
    69  	logger.Info("Connecting to Buildkite...")
    70  	if err := worker.Connect(); err != nil {
    71  		logger.Fatal("%s", err)
    72  	}
    73  
    74  	logger.Info("Agent successfully connected")
    75  	logger.Info("You can press Ctrl-C to stop the agent")
    76  
    77  	if r.AgentConfiguration.DisconnectAfterJob {
    78  		logger.Info("Waiting for job to be assigned...")
    79  		logger.Info("The agent will automatically disconnect after %d seconds if no job is assigned", r.AgentConfiguration.DisconnectAfterJobTimeout)
    80  	} else {
    81  		logger.Info("Waiting for work...")
    82  	}
    83  
    84  	// Start a signalwatcher so we can monitor signals and handle shutdowns
    85  	signalwatcher.Watch(func(sig signalwatcher.Signal) {
    86  		r.signalLock.Lock()
    87  		defer r.signalLock.Unlock()
    88  
    89  		if sig == signalwatcher.QUIT {
    90  			logger.Debug("Received signal `%s`", sig.String())
    91  			worker.Stop(false)
    92  		} else if sig == signalwatcher.TERM || sig == signalwatcher.INT {
    93  			logger.Debug("Received signal `%s`", sig.String())
    94  			if r.interruptCount == 0 {
    95  				r.interruptCount++
    96  				logger.Info("Received CTRL-C, send again to forcefully kill the agent")
    97  				worker.Stop(true)
    98  			} else {
    99  				logger.Info("Forcefully stopping running jobs and stopping the agent")
   100  				worker.Stop(false)
   101  			}
   102  		} else {
   103  			logger.Debug("Ignoring signal `%s`", sig.String())
   104  		}
   105  	})
   106  
   107  	// Starts the agent worker. This will block until the agent has
   108  	// finished or is stopped.
   109  	if err := worker.Start(); err != nil {
   110  		logger.Fatal("%s", err)
   111  	}
   112  
   113  	// Now that the agent has stopped, we can disconnect it
   114  	logger.Info("Disconnecting %s...", worker.Agent.Name)
   115  	worker.Disconnect()
   116  
   117  	return nil
   118  }
   119  
   120  // Takes the options passed to the CLI, and creates an api.Agent record that
   121  // will be sent to the Buildkite Agent API for registration.
   122  func (r *AgentPool) CreateAgentTemplate() *api.Agent {
   123  	agent := &api.Agent{
   124  		Name:              r.Name,
   125  		Priority:          r.Priority,
   126  		Tags:              r.Tags,
   127  		ScriptEvalEnabled: r.AgentConfiguration.CommandEval,
   128  		Version:           Version(),
   129  		Build:             BuildVersion(),
   130  		PID:               os.Getpid(),
   131  		Arch:              runtime.GOARCH,
   132  	}
   133  
   134  	// get a unique identifier for the underlying host
   135  	if machineID, err := machineid.ProtectedID("buildkite-agent"); err != nil {
   136  		logger.Warn("Failed to find unique machine-id: %v", err)
   137  	} else {
   138  		agent.MachineID = machineID
   139  	}
   140  
   141  	// Attempt to add the EC2 tags
   142  	if r.TagsFromEC2 {
   143  		logger.Info("Fetching EC2 meta-data...")
   144  
   145  		err := retry.Do(func(s *retry.Stats) error {
   146  			tags, err := EC2MetaData{}.Get()
   147  			if err != nil {
   148  				logger.Warn("%s (%s)", err, s)
   149  			} else {
   150  				logger.Info("Successfully fetched EC2 meta-data")
   151  				for tag, value := range tags {
   152  					agent.Tags = append(agent.Tags, fmt.Sprintf("%s=%s", tag, value))
   153  				}
   154  				s.Break()
   155  			}
   156  
   157  			return err
   158  		}, &retry.Config{Maximum: 5, Interval: 1 * time.Second, Jitter: true})
   159  
   160  		// Don't blow up if we can't find them, just show a nasty error.
   161  		if err != nil {
   162  			logger.Error(fmt.Sprintf("Failed to fetch EC2 meta-data: %s", err.Error()))
   163  		}
   164  	}
   165  
   166  	// Attempt to add the EC2 tags
   167  	if r.TagsFromEC2Tags {
   168  		logger.Info("Fetching EC2 tags...")
   169  		err := retry.Do(func(s *retry.Stats) error {
   170  			tags, err := EC2Tags{}.Get()
   171  			// EC2 tags are apparently "eventually consistent" and sometimes take several seconds
   172  			// to be applied to instances. This error will cause retries.
   173  			if err == nil && len(tags) == 0 {
   174  				err = errors.New("EC2 tags are empty")
   175  			}
   176  			if err != nil {
   177  				logger.Warn("%s (%s)", err, s)
   178  			} else {
   179  				logger.Info("Successfully fetched EC2 tags")
   180  				for tag, value := range tags {
   181  					agent.Tags = append(agent.Tags, fmt.Sprintf("%s=%s", tag, value))
   182  				}
   183  				s.Break()
   184  			}
   185  			return err
   186  		}, &retry.Config{Maximum: 5, Interval: r.WaitForEC2TagsTimeout / 5, Jitter: true})
   187  
   188  		// Don't blow up if we can't find them, just show a nasty error.
   189  		if err != nil {
   190  			logger.Error(fmt.Sprintf("Failed to find EC2 tags: %s", err.Error()))
   191  		}
   192  	}
   193  
   194  	// Attempt to add the Google Cloud meta-data
   195  	if r.TagsFromGCP {
   196  		tags, err := GCPMetaData{}.Get()
   197  		if err != nil {
   198  			// Don't blow up if we can't find them, just show a nasty error.
   199  			logger.Error(fmt.Sprintf("Failed to fetch Google Cloud meta-data: %s", err.Error()))
   200  		} else {
   201  			for tag, value := range tags {
   202  				agent.Tags = append(agent.Tags, fmt.Sprintf("%s=%s", tag, value))
   203  			}
   204  		}
   205  	}
   206  
   207  	var err error
   208  
   209  	// Add the hostname
   210  	agent.Hostname, err = os.Hostname()
   211  	if err != nil {
   212  		logger.Warn("Failed to find hostname: %s", err)
   213  	}
   214  
   215  	// Add the OS dump
   216  	agent.OS, err = system.VersionDump()
   217  	if err != nil {
   218  		logger.Warn("Failed to find OS information: %s", err)
   219  	}
   220  
   221  	// Attempt to add the host tags
   222  	if r.TagsFromHost {
   223  		agent.Tags = append(agent.Tags,
   224  			fmt.Sprintf("hostname=%s", agent.Hostname),
   225  			fmt.Sprintf("os=%s", runtime.GOOS),
   226  		)
   227  		if agent.MachineID != "" {
   228  			agent.Tags = append(agent.Tags, fmt.Sprintf("machine-id=%s", agent.MachineID))
   229  		}
   230  	}
   231  
   232  	return agent
   233  }
   234  
   235  // Takes the agent template and returns a registered agent. The registered
   236  // agent includes the Access Token used to communicate with the Buildkite Agent
   237  // API
   238  func (r *AgentPool) RegisterAgent(agent *api.Agent) (*api.Agent, error) {
   239  	var registered *api.Agent
   240  	var err error
   241  	var resp *api.Response
   242  
   243  	register := func(s *retry.Stats) error {
   244  		registered, resp, err = r.APIClient.Agents.Register(agent)
   245  		if err != nil {
   246  			if resp != nil && resp.StatusCode == 401 {
   247  				logger.Warn("Buildkite rejected the registration (%s)", err)
   248  				s.Break()
   249  			} else {
   250  				logger.Warn("%s (%s)", err, s)
   251  			}
   252  		}
   253  
   254  		return err
   255  	}
   256  
   257  	// Try to register, retrying every 10 seconds for a maximum of 30 attempts (5 minutes)
   258  	err = retry.Do(register, &retry.Config{Maximum: 30, Interval: 10 * time.Second})
   259  
   260  	return registered, err
   261  }
   262  
   263  // Shows the welcome banner and the configuration options used when starting
   264  // this agent.
   265  func (r *AgentPool) ShowBanner() {
   266  	welcomeMessage :=
   267  		"\n" +
   268  			"%s  _           _ _     _ _    _ _                                _\n" +
   269  			" | |         (_) |   | | |  (_) |                              | |\n" +
   270  			" | |__  _   _ _| | __| | | ___| |_ ___    __ _  __ _  ___ _ __ | |_\n" +
   271  			" | '_ \\| | | | | |/ _` | |/ / | __/ _ \\  / _` |/ _` |/ _ \\ '_ \\| __|\n" +
   272  			" | |_) | |_| | | | (_| |   <| | ||  __/ | (_| | (_| |  __/ | | | |_\n" +
   273  			" |_.__/ \\__,_|_|_|\\__,_|_|\\_\\_|\\__\\___|  \\__,_|\\__, |\\___|_| |_|\\__|\n" +
   274  			"                                                __/ |\n" +
   275  			" http://buildkite.com/agent                    |___/\n%s\n"
   276  
   277  	if logger.ColorsEnabled() {
   278  		fmt.Fprintf(logger.OutputPipe(), welcomeMessage, "\x1b[32m", "\x1b[0m")
   279  	} else {
   280  		fmt.Fprintf(logger.OutputPipe(), welcomeMessage, "", "")
   281  	}
   282  
   283  	logger.Notice("Starting buildkite-agent v%s with PID: %s", Version(), fmt.Sprintf("%d", os.Getpid()))
   284  	logger.Notice("The agent source code can be found here: https://github.com/buildkite/agent")
   285  	logger.Notice("For questions and support, email us at: hello@buildkite.com")
   286  
   287  	if r.ConfigFilePath != "" {
   288  		logger.Info("Configuration loaded from: %s", r.ConfigFilePath)
   289  	}
   290  
   291  	logger.Debug("Bootstrap command: %s", r.AgentConfiguration.BootstrapScript)
   292  	logger.Debug("Build path: %s", r.AgentConfiguration.BuildPath)
   293  	logger.Debug("Hooks directory: %s", r.AgentConfiguration.HooksPath)
   294  	logger.Debug("Plugins directory: %s", r.AgentConfiguration.PluginsPath)
   295  
   296  	if !r.AgentConfiguration.SSHKeyscan {
   297  		logger.Info("Automatic ssh-keyscan has been disabled")
   298  	}
   299  
   300  	if !r.AgentConfiguration.CommandEval {
   301  		logger.Info("Evaluating console commands has been disabled")
   302  	}
   303  
   304  	if !r.AgentConfiguration.PluginsEnabled {
   305  		logger.Info("Plugins have been disabled")
   306  	}
   307  
   308  	if !r.AgentConfiguration.RunInPty {
   309  		logger.Info("Running builds within a pseudoterminal (PTY) has been disabled")
   310  	}
   311  
   312  	if r.AgentConfiguration.DisconnectAfterJob {
   313  		logger.Info("Agent will disconnect after a job run has completed with a timeout of %d seconds", r.AgentConfiguration.DisconnectAfterJobTimeout)
   314  	}
   315  }