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 }