├── .gitignore ├── Dockerfile ├── Dockerfile.golang-agent ├── LICENSE ├── README.md ├── agent ├── agent.go ├── agent_test.go ├── artifacts.go ├── artifacts_test.go ├── build_console.go ├── build_session.go ├── build_session_test.go ├── command_and.go ├── command_cleandir.go ├── command_cleandir_test.go ├── command_compose.go ├── command_cond.go ├── command_download_artifact.go ├── command_echo.go ├── command_exec.go ├── command_export.go ├── command_fail.go ├── command_generate_test_report.go ├── command_mkdirs.go ├── command_or.go ├── command_report.go ├── command_secret.go ├── command_upload_artifact.go ├── config.go ├── diskspace.go ├── diskspace_windows.go ├── logging.go ├── not_implemented.go ├── on_cancel_test.go ├── registration.go ├── run_if_config_test.go ├── states.go ├── test.go ├── unit_test_report_template.go ├── utils.go ├── utils_test.go └── websocket.go ├── build-agent.sh ├── build.sh ├── build └── build.go ├── build_gocd.sh ├── installers ├── deb │ ├── Dockerfile │ ├── build.sh │ ├── package │ │ ├── DEBIAN │ │ │ ├── changelog │ │ │ ├── conffiles │ │ │ ├── control │ │ │ ├── copyright │ │ │ ├── postinst │ │ │ ├── postrm │ │ │ ├── preinst │ │ │ └── prerm │ │ └── etc │ │ │ ├── default │ │ │ └── gocd-golang-agent │ │ │ └── init.d │ │ │ └── gocd-golang-agent │ ├── publish.sh │ └── vars └── rpm │ ├── agent.sh │ ├── build.sh │ ├── gocd-golang-agent.spec │ ├── gocd-golang-agent_default │ ├── gocd-golang-agent_init.d │ └── vars ├── junit ├── test │ ├── junit_illegal_report.xml │ ├── junit_report1.xml │ ├── junit_report2.xml │ └── junit_report3.xml ├── xml_report.go └── xml_report_test.go ├── main.go ├── nunit ├── test │ ├── illegal_report.xml │ ├── nunit2x_report1.xml │ ├── nunit2x_report2.xml │ └── nunit3_report.xml ├── xml_report.go └── xml_report_test.go ├── protocol ├── agent_runtime_info.go ├── build.go ├── build_command.go ├── build_command_test.go ├── message.go ├── registration.go ├── report.go └── websocket.go ├── run_in_docker.sh ├── server ├── artifacts.go ├── cert.go ├── console.go ├── filters.go ├── remote_agent.go ├── responses.go └── server.go ├── start-agent.sh └── stream ├── nop_closer.go ├── prefix_writer.go ├── prefix_writer_test.go ├── substitute_writer.go └── substitute_writer_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | /installers/deb/*.deb 3 | /installers/deb/package/usr/bin/gocd-golang-agent 4 | /gocd-golang-agent 5 | /testreport.xml 6 | .idea/ 7 | *.iml 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:trusty 2 | MAINTAINER GoCD Team 3 | 4 | RUN apt-get update 5 | RUN apt-get -y upgrade 6 | RUN apt-get -y install git software-properties-common python-software-properties 7 | RUN add-apt-repository ppa:ubuntu-lxc/lxd-stable 8 | RUN apt-get update 9 | RUN apt-get -y install golang 10 | 11 | ADD gocd-golang-agent gocd-golang-agent 12 | -------------------------------------------------------------------------------- /Dockerfile.golang-agent: -------------------------------------------------------------------------------- 1 | FROM ubuntu:trusty 2 | MAINTAINER GoCD Team 3 | 4 | # install gocd-golang-agent 5 | RUN echo deb https://dl.bintray.com/alex-hal9000/gocd-golang-agent master main | tee -a /etc/apt/sources.list 6 | RUN apt-get -y install apt-transport-https 7 | RUN apt-get -y update 8 | RUN apt-get -y --force-yes install gocd-golang-agent 9 | 10 | # add ubuntu-lxc apt-get repo (for newer version of golang) 11 | RUN apt-get -y install software-properties-common python-software-properties 12 | RUN add-apt-repository ppa:ubuntu-lxc/lxd-stable 13 | RUN apt-get -y update 14 | 15 | # install project specific packages 16 | RUN apt-get -y install golang git 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GoLang agent for GoCD 2 | ========================= 3 | 4 | [![Join the chat at https://gitter.im/gocd/gocd-golang-agent](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gocd/gocd-golang-agent?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | GOCD agent golang implementation. Comparing to java implementation, golang agent has less installation dependency, less memory footprint and shorter boostrap time. More suitable for running in container. 7 | 8 | Golang agent is based on "BuildCommand API" proposed [here](https://github.com/gocd/gocd/issues/1954). It's still working in progress. If you want to try out the golang agent, please build GoCD server from the latest master branch. 9 | 10 | ### Features not supported yet 11 | * SCM materials other than git 12 | * Java task plugins 13 | * Java scm plugins such as Github-PR 14 | 15 | 16 | ### Installation 17 | 18 | On Ubuntu: 19 | ``` 20 | # Add Bintray's GPG key: 21 | sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 379CE192D401AB61 22 | # Add repo 23 | sudo echo deb https://dl.bintray.com/alex-hal9000/gocd-golang-agent master main | sudo tee -a /etc/apt/sources.list 24 | sudo apt-get update 25 | # Install the package (add '-y --force-yes' after 'install' if automating) 26 | sudo apt-get install gocd-golang-agent 27 | ``` 28 | 29 | ### Configure Agent 30 | 31 | Agent is designed to be configured by environment variables. The followings are available options: 32 | 33 | * **GOCD_SERVER_URL**: Go server url, default to https://localhost:8154/go. 34 | * **GOCD_AGENT_WORKING_DIR**: Agent working directory, default to Agent script launch directory. All build data will be inside this directory. 35 | * **GOCD_AGENT_CONFIG_DIR**: Agent configurations for connecting to Go server, default to be "config" directory inside **GOCD_AGENT_WORKING_DIR** directory 36 | * **GOCD_AGENT_LOG_DIR**: Agent log directory, without this configuration, log will be output to stdout. 37 | * **DEBUG**: set this environment variable to any value will turn on debug log. 38 | 39 | 40 | ### Development 41 | 42 | Check out source 43 | * git clone https://github.com/gocd-contrib/gocd-golang-agent.git src/github.com/gocd-contrib/gocd-golang-agent 44 | 45 | Building the agent binary 46 | * go run src/github.com/gocd-contrib/gocd-golang-agent/build/build.go 47 | 48 | ### Download 49 | Pre-build binary can be found here : https://bintray.com/gocd-contrib 50 | 51 | ## Contributing 52 | 53 | Bug reports and pull requests are welcome on GitHub at https://github.com/gocd-contrib/gocd-golang-agent. 54 | 55 | [Document for Developer](https://github.com/gocd-contrib/gocd-golang-agent/wiki/For-Developer) 56 | 57 | -------------------------------------------------------------------------------- /agent/agent.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "github.com/satori/go.uuid" 22 | "io/ioutil" 23 | "net/http" 24 | "os" 25 | "time" 26 | ) 27 | 28 | var ( 29 | buildSession *BuildSession 30 | logger *Logger 31 | config *Config 32 | AgentId string 33 | ) 34 | 35 | func LogDebug(format string, v ...interface{}) { 36 | logger.Debug.Printf(format, v...) 37 | } 38 | 39 | func LogInfo(format string, v ...interface{}) { 40 | logger.Info.Printf(format, v...) 41 | } 42 | 43 | func GetConfig() *Config { 44 | return config 45 | } 46 | 47 | func Initialize() { 48 | config = LoadConfig() 49 | logger = MakeLogger(config.LogDir, "gocd-golang-agent.log", config.OutputDebugLog) 50 | LogInfo(">>>>>>> go >>>>>>>") 51 | LogInfo("working directory: %v", config.WorkingDir) 52 | if _, err := os.Stat(config.WorkingDir); err != nil { 53 | logger.Error.Fatal(err) 54 | } 55 | 56 | if err := Mkdirs(config.ConfigDir); err != nil { 57 | logger.Error.Fatal(err) 58 | } 59 | 60 | if _, err := os.Stat(config.AgentIdFile); err == nil { 61 | data, err2 := ioutil.ReadFile(config.AgentIdFile) 62 | if err2 != nil { 63 | logger.Error.Printf("failed to read uuid file(%v): %v", config.AgentIdFile, err2) 64 | } else { 65 | AgentId = string(data) 66 | } 67 | } 68 | if AgentId == "" { 69 | AgentId = uuid.NewV4().String() 70 | ioutil.WriteFile(config.AgentIdFile, []byte(AgentId), 0644) 71 | } 72 | } 73 | 74 | func Start() error { 75 | err := Register() 76 | if err != nil { 77 | return err 78 | } 79 | 80 | httpClient, err := GoServerRemoteClient(true) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | conn, err := MakeWebsocketConnection(config.WssServerURL(), config.HttpsServerURL()) 86 | if err != nil { 87 | return err 88 | } 89 | defer conn.Close() 90 | defer closeBuildSession() 91 | 92 | pingTick := time.NewTicker(10 * time.Second) 93 | ping(conn.Send) 94 | for { 95 | select { 96 | case <-pingTick.C: 97 | ping(conn.Send) 98 | case msg, ok := <-conn.Received: 99 | if !ok { 100 | return Err("Websocket connection is closed") 101 | } 102 | err := processMessage(msg, httpClient, conn.Send) 103 | if err != nil { 104 | return err 105 | } 106 | } 107 | } 108 | } 109 | 110 | func processMessage(msg *protocol.Message, httpClient *http.Client, send chan *protocol.Message) error { 111 | switch msg.Action { 112 | case protocol.SetCookieAction: 113 | SetState("cookie", msg.DataString()) 114 | case protocol.CancelBuildAction: 115 | closeBuildSession() 116 | case protocol.ReregisterAction: 117 | CleanRegistration() 118 | return Err("received reregister message") 119 | case protocol.BuildAction: 120 | closeBuildSession() 121 | build := msg.DataBuild() 122 | SetState("buildLocator", build.BuildLocator) 123 | SetState("buildLocatorForDisplay", build.BuildLocatorForDisplay) 124 | curl, err := config.MakeFullServerURL(build.ConsoleUrl) 125 | if err != nil { 126 | return err 127 | } 128 | aurl, err := config.MakeFullServerURL(build.ArtifactUploadBaseUrl) 129 | if err != nil { 130 | return err 131 | } 132 | buildSession = MakeBuildSession( 133 | build.BuildId, 134 | build.BuildCommand, 135 | MakeBuildConsole(httpClient, curl), 136 | &Artifacts{httpClient: httpClient}, 137 | aurl, 138 | send, 139 | config.WorkingDir, 140 | ) 141 | buildSession.ReplaceEcho("${agent.location}", config.WorkingDir) 142 | buildSession.ReplaceEcho("${agent.hostname}", config.Hostname) 143 | buildSession.ReplaceEcho("${date}", func() string { return time.Now().Format("2006-01-02 15:04:05 PDT") }) 144 | go processBuild(send, buildSession) 145 | default: 146 | panic(Sprintf("Unknown message action: %+v", msg)) 147 | } 148 | return nil 149 | } 150 | 151 | func processBuild(send chan *protocol.Message, buildSession *BuildSession) { 152 | defer func() { 153 | SetState("runtimeStatus", "Idle") 154 | ping(send) 155 | logger.Debug.Printf("! exit goroutine: process build command message") 156 | }() 157 | SetState("runtimeStatus", "Building") 158 | ping(send) 159 | buildSession.Run() 160 | LogInfo("done") 161 | } 162 | 163 | func ping(send chan *protocol.Message) { 164 | send <- protocol.PingMessage(GetAgentRuntimeInfo()) 165 | } 166 | 167 | func closeBuildSession() { 168 | if buildSession != nil { 169 | buildSession.Close() 170 | buildSession = nil 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /agent/agent_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package agent_test 17 | 18 | import ( 19 | "bytes" 20 | "crypto/tls" 21 | "flag" 22 | . "github.com/gocd-contrib/gocd-golang-agent/agent" 23 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 24 | "github.com/gocd-contrib/gocd-golang-agent/server" 25 | "github.com/xli/assert" 26 | "io" 27 | "io/ioutil" 28 | "net/http" 29 | "os" 30 | "path/filepath" 31 | "runtime" 32 | "strings" 33 | "sync" 34 | "testing" 35 | "time" 36 | ) 37 | 38 | var ( 39 | testFileContentMD5 = "41e43efb30d3fbfcea93542157809ac0" 40 | 41 | goServerUrl string 42 | goServer *server.Server 43 | stateLog *StateLog 44 | buildId string 45 | agentStopped chan bool 46 | ) 47 | 48 | func TestSetCookieAfterConnected(t *testing.T) { 49 | setUp(t) 50 | defer tearDown() 51 | goServer.SendBuild(AgentId, buildId, protocol.EchoCommand("hello")) 52 | 53 | assert.Equal(t, "agent Building", stateLog.Next()) 54 | assert.NotEqual(t, "", GetState("cookie")) 55 | assert.Equal(t, "build Passed", stateLog.Next()) 56 | assert.Equal(t, "agent Idle", stateLog.Next()) 57 | } 58 | 59 | func TestMain(m *testing.M) { 60 | flag.Parse() 61 | 62 | workingDir, err := ioutil.TempDir("", "gocd-golang-agent") 63 | if err != nil { 64 | panic(err) 65 | } 66 | println("working directories:", workingDir) 67 | serverWorkingDir := filepath.Join(workingDir, "server") 68 | agentWorkingDir := filepath.Join(workingDir, "agent") 69 | 70 | err = Mkdirs(serverWorkingDir) 71 | if err != nil { 72 | panic(err) 73 | } 74 | err = Mkdirs(agentWorkingDir) 75 | if err != nil { 76 | panic(err) 77 | } 78 | 79 | startServer(serverWorkingDir) 80 | BuildDebugToConsoleLog = false 81 | os.Setenv("DEBUG", "t") 82 | os.Setenv("GOCD_SERVER_URL", goServerUrl) 83 | os.Setenv("GOCD_SERVER_WEB_SOCKET_PATH", server.WebSocketPath) 84 | os.Setenv("GOCD_SERVER_REGISTRATION_PATH", server.RegistrationPath) 85 | os.Setenv("GOCD_AGENT_WORKING_DIR", agentWorkingDir) 86 | os.Setenv("GOCD_AGENT_LOG_DIR", agentWorkingDir) 87 | 88 | Initialize() 89 | 90 | os.Exit(m.Run()) 91 | } 92 | 93 | func startServer(workingDir string) { 94 | certFile := filepath.Join(workingDir, "cert.pem") 95 | keyFile := filepath.Join(workingDir, "private.pem") 96 | cert := server.NewCert("localhost") 97 | err := cert.Generate(certFile, keyFile) 98 | if err != nil { 99 | panic(err) 100 | } 101 | address := cert.Host + ":1234" 102 | stateLog = &StateLog{states: make(chan string)} 103 | goServerUrl = "https://" + address 104 | goServer = server.New(address, 105 | certFile, 106 | keyFile, 107 | workingDir, 108 | MakeLogger(workingDir, "server.log", true).Info) 109 | goServer.StateListeners = []server.StateListener{stateLog} 110 | 111 | go func() { 112 | e := goServer.Start() 113 | panic(e.Error()) 114 | }() 115 | 116 | println("wait for server started") 117 | if err := waitForServerStarted(goServerUrl + server.StatusPath); err != nil { 118 | panic(err) 119 | } 120 | println("server started") 121 | } 122 | 123 | func waitForServerStarted(url string) error { 124 | tr := &http.Transport{ 125 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 126 | } 127 | client := &http.Client{Transport: tr} 128 | timeout := time.After(5 * time.Second) 129 | for { 130 | select { 131 | case <-timeout: 132 | return Err("wait for server start timeout") 133 | default: 134 | _, err := client.Get(url) 135 | if err == nil { 136 | return nil 137 | } 138 | } 139 | } 140 | } 141 | 142 | type StateLog struct { 143 | states chan string 144 | mu sync.Mutex 145 | buildId, agentId string 146 | } 147 | 148 | func (log *StateLog) Notify(class, id, state string) { 149 | log.mu.Lock() 150 | defer log.mu.Unlock() 151 | switch class { 152 | case "agent": 153 | if id == log.agentId { 154 | log.notify("agent " + state) 155 | } 156 | case "build": 157 | if id == log.buildId { 158 | log.notify("build " + state) 159 | } 160 | } 161 | } 162 | 163 | func (log *StateLog) notify(state string) { 164 | select { 165 | case log.states <- state: 166 | case <-time.After(1 * time.Second): 167 | } 168 | } 169 | 170 | func (log *StateLog) Next() string { 171 | select { 172 | case state := <-log.states: 173 | return state 174 | case <-time.After(1 * time.Second): 175 | return "timeout" 176 | } 177 | } 178 | 179 | func (log *StateLog) Reset(buildId, agentId string) { 180 | log.mu.Lock() 181 | defer log.mu.Unlock() 182 | log.buildId = buildId 183 | log.agentId = agentId 184 | } 185 | 186 | func contains(s1, s2 string) bool { 187 | return strings.Contains(s1, s2) 188 | } 189 | 190 | func split(s1, s2 string) []string { 191 | return strings.Split(s1, s2) 192 | } 193 | 194 | func startWith(s1, s2 string) bool { 195 | return strings.HasPrefix(s1, s2) 196 | } 197 | 198 | func trimTimestamp(log string) string { 199 | lines := strings.Split(log, "\n") 200 | var buf bytes.Buffer 201 | for _, l := range lines { 202 | if len(l) > 13 { 203 | buf.WriteString(l[13:]) 204 | buf.WriteString("\n") 205 | } 206 | } 207 | return buf.String() 208 | } 209 | 210 | func createPipelineDir() string { 211 | dir := pipelineDir() 212 | err := Mkdirs(dir) 213 | if err != nil { 214 | panic(err) 215 | } 216 | return dir 217 | } 218 | 219 | func createTestProjectInPipelineDir() string { 220 | root := createPipelineDir() 221 | createTestProject(root) 222 | return root 223 | } 224 | 225 | func createTestProject(root string) { 226 | err := Mkdirs(root + "/src/hello") 227 | if err != nil { 228 | panic(err) 229 | } 230 | err = Mkdirs(root + "/test/world") 231 | if err != nil { 232 | panic(err) 233 | } 234 | createTestFile(root, "0.txt") 235 | createTestFile(root+"/src", "1.txt") 236 | createTestFile(root+"/src", "2.txt") 237 | createTestFile(root+"/src/hello", "3.txt") 238 | createTestFile(root+"/src/hello", "4.txt") 239 | createTestFile(root+"/test", "5.txt") 240 | createTestFile(root+"/test", "6.txt") 241 | createTestFile(root+"/test", "7.txt") 242 | createTestFile(root+"/test/world", "8.txt") 243 | createTestFile(root+"/test/world", "9.txt") 244 | createTestFile(root+"/test/world", "10.txt") 245 | createTestFile(root+"/test/world", "11.txt") 246 | createTestFile(root+"/test/world2", "10.txt") 247 | createTestFile(root+"/test/world2", "11.txt") 248 | } 249 | 250 | func createTestFile(dir, fname string) string { 251 | err := writeFile(dir, fname, "file created for test") 252 | if err != nil { 253 | panic(err) 254 | } 255 | return fname 256 | } 257 | 258 | func writeFile(dir, fname, content string) error { 259 | err := Mkdirs(dir) 260 | if err != nil { 261 | return err 262 | } 263 | fpath := filepath.Join(dir, fname) 264 | f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE, 0644) 265 | if err != nil { 266 | return err 267 | } 268 | data := []byte(content) 269 | n, err := f.Write(data) 270 | if err == nil && n < len(data) { 271 | return io.ErrShortWrite 272 | } 273 | return f.Close() 274 | } 275 | 276 | func pipelineDir() string { 277 | return Join("/", os.Getenv("GOCD_AGENT_WORKING_DIR"), pipelineDirRelativePath()) 278 | } 279 | 280 | func pipelineDirRelativePath() string { 281 | return Join("/", "pipelines", buildId) 282 | } 283 | 284 | func relativePath(wd string) string { 285 | return wd[len(os.Getenv("GOCD_AGENT_WORKING_DIR"))+1:] 286 | } 287 | 288 | func startAgent(t *testing.T) chan bool { 289 | done := make(chan bool) 290 | go func() { 291 | err := Start() 292 | if err.Error() != "received reregister message" { 293 | t.Error("Unexpected error to quit agent: ", err) 294 | } 295 | close(done) 296 | }() 297 | assert.Equal(t, "agent Idle", stateLog.Next()) 298 | return done 299 | } 300 | 301 | func setUp(t *testing.T) { 302 | pc, _, _, _ := runtime.Caller(1) 303 | _func := runtime.FuncForPC(pc) 304 | parts := strings.Split(_func.Name(), ".") 305 | 306 | buildId = parts[len(parts)-1] 307 | stateLog.Reset(buildId, AgentId) 308 | agentStopped = startAgent(t) 309 | } 310 | 311 | func tearDown() { 312 | goServer.Send(AgentId, protocol.ReregisterMessage()) 313 | select { 314 | case <-time.After(5 * time.Second): 315 | panic("wait for agent stop timeout") 316 | case <-agentStopped: 317 | } 318 | 319 | err := os.RemoveAll(pipelineDir()) 320 | if err != nil { 321 | println("WARN: clean up pipeline directory failed:", err.Error()) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /agent/artifacts.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "archive/zip" 21 | "bytes" 22 | "io" 23 | "io/ioutil" 24 | "mime/multipart" 25 | "net/http" 26 | "net/url" 27 | "os" 28 | "path/filepath" 29 | "strconv" 30 | "strings" 31 | "time" 32 | ) 33 | 34 | type Artifacts struct { 35 | httpClient *http.Client 36 | } 37 | 38 | func (u *Artifacts) DownloadFile(source *url.URL, destPath string) (err error) { 39 | dir, _ := filepath.Split(destPath) 40 | err = Mkdirs(dir) 41 | if err != nil { 42 | return err 43 | } 44 | destFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE, 0644) 45 | if err != nil { 46 | return 47 | } 48 | return u.downloadFile(source, destFile) 49 | } 50 | 51 | func (u *Artifacts) DownloadDir(source *url.URL, destPath string) error { 52 | zipfile, err := ioutil.TempFile("", "tmp.zip") 53 | if err != nil { 54 | return err 55 | } 56 | defer os.Remove(zipfile.Name()) 57 | LogDebug("tmp file created for download zipped dir") 58 | err = u.downloadFile(source, zipfile) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | zipReader, err := zip.OpenReader(zipfile.Name()) 64 | if err != nil { 65 | return err 66 | } 67 | LogDebug("unzip to %v", destPath) 68 | defer zipReader.Close() 69 | destDir := filepath.Dir(destPath) 70 | for _, file := range zipReader.File { 71 | dest := filepath.Join(destDir, file.FileHeader.Name) 72 | if file.FileHeader.FileInfo().IsDir() { 73 | LogDebug("mkdirs %v", dest) 74 | err = Mkdirs(dest) 75 | } else { 76 | LogDebug("extract file %v => %v", file.FileHeader.Name, dest) 77 | err = u.extractFile(file, dest) 78 | } 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | LogDebug("unzip finished") 84 | return nil 85 | } 86 | 87 | func (u *Artifacts) downloadFile(source *url.URL, destFile *os.File) (err error) { 88 | defer destFile.Close() 89 | LogDebug("download file %v => %v", source, destFile.Name()) 90 | retry := 0 91 | startDownload: 92 | resp, err := u.httpClient.Get(source.String()) 93 | if err != nil { 94 | return 95 | } 96 | LogDebug("response: %v", resp.Status) 97 | if resp.StatusCode == http.StatusAccepted { 98 | LogDebug("Server responsed StatusAccepted, sleep 1 sec and start download again") 99 | time.Sleep(1 * time.Second) 100 | goto startDownload 101 | } 102 | if resp.StatusCode != http.StatusOK { 103 | if retry < 3 { 104 | retry++ 105 | LogDebug("sleep %v sec and start download again", retry) 106 | time.Sleep(time.Duration(retry) * time.Second) 107 | goto startDownload 108 | } else { 109 | return Err("tried %v times to download [%v] and all failed.", retry, source) 110 | } 111 | } 112 | defer resp.Body.Close() 113 | _, err = io.Copy(destFile, resp.Body) 114 | return 115 | } 116 | 117 | func (u *Artifacts) VerifyChecksum(srcPath, destPath, checksumFname string) error { 118 | destInfo, err := os.Stat(destPath) 119 | if err != nil { 120 | return err 121 | } 122 | if destInfo.IsDir() { 123 | return filepath.Walk(destPath, func(path string, info os.FileInfo, err error) error { 124 | if err != nil { 125 | return err 126 | } 127 | if info.IsDir() { 128 | return nil 129 | } 130 | srcFname := Join("/", srcPath, path[len(destPath)+1:]) 131 | return u.VerifyChecksumFile(srcFname, path, checksumFname) 132 | }) 133 | } else { 134 | return u.VerifyChecksumFile(srcPath, destPath, checksumFname) 135 | } 136 | } 137 | 138 | func (u *Artifacts) VerifyChecksumFile(srcFname, fname, checksumFname string) error { 139 | md5, err := ComputeMd5(fname) 140 | if err != nil { 141 | return err 142 | } 143 | checksum, err := ioutil.ReadFile(checksumFname) 144 | if err != nil { 145 | return err 146 | } 147 | properties := ParseChecksum(string(checksum)) 148 | // Convert path used as key name in properties, because md5.checksum always has unix / slashes 149 | srcFname=filepath.ToSlash(srcFname) 150 | if properties[srcFname] == "" { 151 | return Err("[WARN] The md5checksum value of the artifact [%v] was not found on the server. Hence, Go could not verify the integrity of its contents.", srcFname) 152 | } else if properties[srcFname] != md5 { 153 | return Err("[ERROR] Verification of the integrity of the artifact [%v] failed. The artifact file on the server may have changed since its original upload.", srcFname) 154 | } else { 155 | return nil 156 | } 157 | } 158 | 159 | func (u *Artifacts) Upload(source, destPath string, destURL *url.URL) (err error) { 160 | zipped, checksum, err := u.zipSource(source, destPath) 161 | defer os.Remove(zipped) 162 | if err != nil { 163 | return 164 | } 165 | var body bytes.Buffer 166 | writer := multipart.NewWriter(&body) 167 | err = u.writeFilePart(writer, zipped, "zipfile") 168 | if err != nil { 169 | return 170 | } 171 | err = u.writePart(writer, bytes.NewBufferString(checksum), "file_checksum", "checksum_file") 172 | if err != nil { 173 | return 174 | } 175 | err = writer.Close() 176 | if err != nil { 177 | return 178 | } 179 | 180 | attempt := 1 181 | tryPost: 182 | attemptUrl := AppendUrlParam(destURL, "attempt", strconv.Itoa(attempt)) 183 | statusCode, err := u.post(source, writer.FormDataContentType(), attemptUrl, &body) 184 | // client side errors, no retry 185 | if err != nil { 186 | return 187 | } 188 | // success 189 | if statusCode == http.StatusCreated { 190 | return 191 | } 192 | // handle errors 193 | if statusCode == http.StatusRequestEntityTooLarge { 194 | info, _ := os.Stat(zipped) 195 | return Err("Artifact upload for file %s (Size: %d) was denied by the server. This usually happens when server runs out of disk space.", source, info.Size()) 196 | } 197 | // retry for other errors 198 | if attempt < 3 { 199 | attempt++ 200 | goto tryPost 201 | } 202 | return Err("Failed to upload %v. Server response: %v", source, statusCode) 203 | } 204 | 205 | func (u *Artifacts) post(source, contentType string, destURL *url.URL, body *bytes.Buffer) (statusCode int, err error) { 206 | req, err := http.NewRequest("POST", destURL.String(), body) 207 | if err != nil { 208 | return 209 | } 210 | req.Header.Add("Content-Type", contentType) 211 | req.Header.Add("Confirm","true") 212 | 213 | resp, err := u.httpClient.Do(req) 214 | if err != nil { 215 | return 216 | } 217 | return resp.StatusCode, nil 218 | } 219 | 220 | func (u *Artifacts) writeFilePart(writer *multipart.Writer, path, paramName string) error { 221 | file, err := os.Open(path) 222 | if err != nil { 223 | return err 224 | } 225 | defer file.Close() 226 | return u.writePart(writer, file, paramName, filepath.Base(path)) 227 | } 228 | 229 | func (u *Artifacts) writePart(writer *multipart.Writer, src io.Reader, fieldname, filename string) error { 230 | part, err := writer.CreateFormFile(fieldname, filename) 231 | if err != nil { 232 | return err 233 | } 234 | _, err = io.Copy(part, src) 235 | return err 236 | } 237 | 238 | func (u *Artifacts) zipSource(source string, dest string) (string, string, error) { 239 | zipfile, err := ioutil.TempFile("", "tmp.zip") 240 | if err != nil { 241 | return "", "", err 242 | } 243 | defer zipfile.Close() 244 | w := zip.NewWriter(zipfile) 245 | defer w.Close() 246 | 247 | var checksum bytes.Buffer 248 | checksum.WriteString(Sprintf("#\n#%v\n", time.Now())) 249 | err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error { 250 | if err != nil { 251 | return err 252 | } 253 | if info.IsDir() { 254 | return nil 255 | } 256 | 257 | destFile := dest 258 | if path != source { 259 | // source is a directory, find relative path 260 | // from source and attach to dest path 261 | rel := path[len(source):] 262 | if strings.HasPrefix(rel, string(os.PathSeparator)) { 263 | rel = rel[1:] 264 | } 265 | if dest == "" { 266 | destFile = rel 267 | } else { 268 | destFile = dest + "/" + rel 269 | } 270 | } 271 | // Convert slash to Linux slash especally on Windows 272 | destFile=filepath.ToSlash(destFile) 273 | md5, err := ComputeMd5(path) 274 | if err != nil { 275 | return err 276 | } 277 | checksum.WriteString(Sprintf("%v=%v\n", destFile, md5)) 278 | 279 | file, err := os.Open(path) 280 | if err != nil { 281 | return err 282 | } 283 | defer file.Close() 284 | writer, err := w.Create(destFile) 285 | if err != nil { 286 | return err 287 | } 288 | 289 | _, err = io.Copy(writer, file) 290 | return err 291 | }) 292 | return zipfile.Name(), checksum.String(), err 293 | } 294 | 295 | func (u *Artifacts) extractFile(file *zip.File, dest string) error { 296 | rc, err := file.Open() 297 | if err != nil { 298 | return err 299 | } 300 | defer rc.Close() 301 | 302 | err = os.MkdirAll(filepath.Dir(dest), 0755) 303 | if err != nil { 304 | return err 305 | } 306 | destFile, err := os.Create(dest) 307 | if err != nil { 308 | return err 309 | } 310 | defer destFile.Close() 311 | _, err = io.Copy(destFile, rc) 312 | return err 313 | } 314 | -------------------------------------------------------------------------------- /agent/build_console.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "bytes" 21 | "github.com/gocd-contrib/gocd-golang-agent/stream" 22 | "io/ioutil" 23 | "net/http" 24 | "net/url" 25 | "time" 26 | ) 27 | 28 | type BuildConsole struct { 29 | Url *url.URL 30 | HttpClient *http.Client 31 | buffer *bytes.Buffer 32 | stop chan bool 33 | closed chan bool 34 | write chan []byte 35 | } 36 | 37 | func timestampPrefix() []byte { 38 | ts := time.Now().Format("15:04:05.000 ") 39 | return []byte(ts) 40 | } 41 | 42 | func MakeBuildConsole(httpClient *http.Client, url *url.URL) *BuildConsole { 43 | console := BuildConsole{ 44 | HttpClient: httpClient, 45 | Url: url, 46 | buffer: bytes.NewBuffer(make([]byte, 0, 10*1024)), 47 | 48 | stop: make(chan bool), 49 | closed: make(chan bool), 50 | write: make(chan []byte), 51 | } 52 | go func() { 53 | defer func() { 54 | close(console.closed) 55 | LogInfo("build console closed") 56 | }() 57 | tw := stream.NewPrefixWriter(console.buffer, timestampPrefix) 58 | flushTick := time.NewTicker(5 * time.Second) 59 | defer flushTick.Stop() 60 | for { 61 | select { 62 | case log := <-console.write: 63 | tw.Write(log) 64 | case <-console.stop: 65 | console.Flush() 66 | return 67 | case <-flushTick.C: 68 | console.Flush() 69 | } 70 | } 71 | }() 72 | 73 | return &console 74 | } 75 | 76 | func (console *BuildConsole) Close() error { 77 | return closeAndWait(console.stop, console.closed, CancelCommandTimeout) 78 | } 79 | 80 | func (console *BuildConsole) Write(data []byte) (int, error) { 81 | console.write <- data 82 | return len(data), nil 83 | } 84 | 85 | func (console *BuildConsole) Flush() { 86 | if console.buffer.Len() == 0 { 87 | return 88 | } 89 | LogDebug("ConsoleLog: \n%v", console.buffer.String()) 90 | 91 | req := http.Request{ 92 | Method: http.MethodPut, 93 | URL: console.Url, 94 | Body: ioutil.NopCloser(console.buffer), 95 | ContentLength: int64(console.buffer.Len()), 96 | Close: true, 97 | } 98 | _, err := console.HttpClient.Do(&req) 99 | if err != nil { 100 | logger.Error.Printf("build console flush failed: %v", err) 101 | } 102 | console.buffer.Reset() 103 | } 104 | -------------------------------------------------------------------------------- /agent/build_session.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "bytes" 21 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 22 | "github.com/gocd-contrib/gocd-golang-agent/stream" 23 | "io" 24 | "net/url" 25 | "os" 26 | "path/filepath" 27 | "strings" 28 | "time" 29 | ) 30 | 31 | const ( 32 | DefaultSecretMask = "********" 33 | DefaultCancelCommandTimeout = 25 * time.Second 34 | ) 35 | 36 | var ( 37 | CancelCommandTimeout = DefaultCancelCommandTimeout 38 | CancelBuildTimeout = 30 * time.Second 39 | BuildDebugToConsoleLog = true 40 | ) 41 | 42 | type Executor func(session *BuildSession, cmd *protocol.BuildCommand) error 43 | 44 | func Executors() map[string]Executor { 45 | return map[string]Executor{ 46 | protocol.CommandExport: CommandExport, 47 | protocol.CommandEcho: CommandEcho, 48 | protocol.CommandSecret: CommandSecret, 49 | protocol.CommandReportCurrentStatus: CommandReport, 50 | protocol.CommandReportCompleting: CommandReport, 51 | protocol.CommandCompose: CommandCompose, 52 | protocol.CommandCond: CommandCond, 53 | protocol.CommandAnd: CommandAnd, 54 | protocol.CommandOr: CommandOr, 55 | protocol.CommandTest: CommandTest, 56 | protocol.CommandExec: CommandExec, 57 | protocol.CommandMkdirs: CommandMkdirs, 58 | protocol.CommandCleandir: CommandCleandir, 59 | protocol.CommandUploadArtifact: CommandUploadArtifact, 60 | protocol.CommandDownloadFile: CommandDownloadArtifact, 61 | protocol.CommandDownloadDir: CommandDownloadArtifact, 62 | protocol.CommandFail: CommandFail, 63 | protocol.CommandGenerateTestReport: CommandGenerateTestReport, 64 | protocol.CommandGenerateProperty: NotImplemented, 65 | } 66 | } 67 | 68 | type BuildSession struct { 69 | send chan *protocol.Message 70 | console io.WriteCloser 71 | artifacts *Artifacts 72 | command *protocol.BuildCommand 73 | artifactUploadBaseURL *url.URL 74 | 75 | envs map[string]string 76 | cancel chan bool 77 | done chan bool 78 | echo *stream.SubstituteWriter 79 | secrets *stream.SubstituteWriter 80 | 81 | buildId string 82 | buildStatus string 83 | 84 | rootDir string 85 | wd string 86 | 87 | executors map[string]Executor 88 | } 89 | 90 | func MakeBuildSession(buildId string, 91 | command *protocol.BuildCommand, 92 | console io.WriteCloser, 93 | artifacts *Artifacts, 94 | artifactUploadBaseURL *url.URL, 95 | send chan *protocol.Message, 96 | rootDir string) *BuildSession { 97 | 98 | secrets := stream.NewSubstituteWriter(console) 99 | return &BuildSession{ 100 | buildId: buildId, 101 | buildStatus: protocol.BuildPassed, 102 | console: console, 103 | artifacts: artifacts, 104 | artifactUploadBaseURL: artifactUploadBaseURL, 105 | command: command, 106 | send: send, 107 | envs: make(map[string]string), 108 | cancel: make(chan bool), 109 | done: make(chan bool), 110 | secrets: secrets, 111 | echo: stream.NewSubstituteWriter(secrets), 112 | rootDir: rootDir, 113 | executors: Executors(), 114 | } 115 | } 116 | 117 | func (s *BuildSession) Close() error { 118 | return closeAndWait(s.cancel, s.done, CancelBuildTimeout) 119 | } 120 | 121 | func (s *BuildSession) isCanceled() bool { 122 | if s.buildStatus == protocol.BuildCanceled { 123 | return true 124 | } 125 | if isClosedChan(s.cancel) { 126 | s.buildStatus = protocol.BuildCanceled 127 | return true 128 | } else { 129 | return false 130 | } 131 | } 132 | 133 | func (s *BuildSession) Run() error { 134 | defer func() { 135 | s.console.Close() 136 | s.send <- protocol.CompletedMessage(s.Report("")) 137 | LogInfo("Build completed") 138 | }() 139 | LogInfo("Build started, root directory: %v", s.rootDir) 140 | return s.ProcessCommand() 141 | } 142 | 143 | func (s *BuildSession) ProcessCommand() error { 144 | defer func() { 145 | close(s.done) 146 | }() 147 | 148 | return s.process(s.command) 149 | } 150 | 151 | func (s *BuildSession) process(cmd *protocol.BuildCommand) (err error) { 152 | defer s.onCancel(cmd) 153 | 154 | if s.isCanceled() { 155 | s.debugLog("build canceled, ignore %v", cmd.Name) 156 | return nil 157 | } 158 | 159 | if !cmd.RunIfAny() && !cmd.RunIfMatch(s.buildStatus) { 160 | s.debugLog("ignore %v: build[%v] != runIf[%v]", cmd.Name, s.buildStatus, cmd.RunIfConfig) 161 | //skip, no failure 162 | return nil 163 | } 164 | s.debugLog("process: %v", cmd.Name) 165 | if s.testFailed(cmd.Test) { 166 | return nil 167 | } 168 | 169 | err = s.doProcess(cmd) 170 | if s.isCanceled() { 171 | LogInfo("build canceled") 172 | s.buildStatus = protocol.BuildCanceled 173 | } else if err != nil && s.buildStatus != protocol.BuildFailed { 174 | s.buildStatus = protocol.BuildFailed 175 | errMsg := Sprintf("ERROR: %v\n", err) 176 | LogInfo(errMsg) 177 | s.ConsoleLog(errMsg) 178 | } 179 | 180 | return 181 | } 182 | 183 | func (s *BuildSession) doProcess(cmd *protocol.BuildCommand) error { 184 | s.wd = filepath.Clean(filepath.Join(s.rootDir, cmd.WorkingDirectory)) 185 | s.debugLog("set wd to %v", s.wd) 186 | 187 | if !strings.HasPrefix(s.wd, s.rootDir) { 188 | return Err("Working directory[%v] is outside the agent sandbox.", s.wd) 189 | } 190 | _, err := os.Stat(s.wd) 191 | if err != nil { 192 | if os.IsNotExist(err) { 193 | return Err("Working directory \"%v\" is not a directory", s.wd) 194 | } else { 195 | return err 196 | } 197 | } 198 | 199 | exec := s.executors[cmd.Name] 200 | if exec == nil { 201 | return Err("Unknown build command: %v", cmd.Name) 202 | } else { 203 | return exec(s, cmd) 204 | } 205 | } 206 | 207 | func (s *BuildSession) testFailed(test *protocol.BuildCommand) bool { 208 | if test == nil { 209 | return false 210 | } 211 | s.debugLog("test: %+v", test) 212 | _, err := s.processTestCommand(test) 213 | if s.isCanceled() { 214 | s.debugLog("test is canceled due to build is canceled") 215 | return true 216 | } 217 | if err == nil { 218 | s.debugLog("test matches expectation") 219 | return false 220 | } else { 221 | s.debugLog("test failed with err: %v", err) 222 | return true 223 | } 224 | } 225 | 226 | func (s *BuildSession) onCancel(cmd *protocol.BuildCommand) { 227 | if cmd.OnCancel == nil || !s.isCanceled() { 228 | return 229 | } 230 | cancel := &BuildSession{ 231 | buildId: s.buildId, 232 | console: s.console, 233 | artifacts: s.artifacts, 234 | artifactUploadBaseURL: s.artifactUploadBaseURL, 235 | send: s.send, 236 | envs: s.envs, 237 | secrets: s.secrets, 238 | echo: s.echo, 239 | rootDir: s.rootDir, 240 | executors: s.executors, 241 | command: cmd.OnCancel, 242 | buildStatus: protocol.BuildPassed, 243 | cancel: make(chan bool), 244 | done: make(chan bool), 245 | } 246 | go func() { 247 | cancel.ProcessCommand() 248 | }() 249 | select { 250 | case <-cancel.done: 251 | case <-time.After(CancelCommandTimeout): 252 | s.warn("Kill cancel task because it did not finish in %v.", CancelCommandTimeout) 253 | cancel.Close() 254 | } 255 | } 256 | 257 | func (s *BuildSession) processTestCommand(cmd *protocol.BuildCommand) (bytes.Buffer, error) { 258 | var output bytes.Buffer 259 | session := &BuildSession{ 260 | buildId: s.buildId, 261 | artifacts: s.artifacts, 262 | artifactUploadBaseURL: s.artifactUploadBaseURL, 263 | send: s.send, 264 | envs: s.envs, 265 | secrets: s.secrets.Filter(&output), 266 | echo: s.echo.Filter(&output), 267 | rootDir: s.rootDir, 268 | executors: s.executors, 269 | console: stream.NopCloser(&output), 270 | command: cmd, 271 | buildStatus: protocol.BuildPassed, 272 | cancel: s.cancel, 273 | done: make(chan bool), 274 | } 275 | 276 | err := session.ProcessCommand() 277 | return output, err 278 | } 279 | 280 | func (s *BuildSession) Report(jobState string) *protocol.Report { 281 | return &protocol.Report{ 282 | AgentRuntimeInfo: GetAgentRuntimeInfo(), 283 | BuildId: s.buildId, 284 | JobState: jobState, 285 | Result: s.buildStatus, 286 | } 287 | } 288 | 289 | func (s *BuildSession) ConsoleLog(format string, a ...interface{}) { 290 | s.console.Write([]byte(Sprintf(format, a...))) 291 | } 292 | 293 | func (s *BuildSession) ReplaceEcho(name string, value interface{}) { 294 | s.echo.Substitutions[name] = value 295 | } 296 | 297 | func (s *BuildSession) Env() []string { 298 | osEnv := os.Environ() 299 | bsEnv := make([]string, 0, len(s.envs)+len(osEnv)) 300 | bsEnv = append(bsEnv, osEnv...) 301 | for key, value := range s.envs { 302 | bsEnv = append(bsEnv, Sprintf("%v=%v", key, value)) 303 | } 304 | return bsEnv 305 | } 306 | 307 | func (s *BuildSession) warn(format string, a ...interface{}) { 308 | s.ConsoleLog(Sprintf("WARN: %v\n", format), a...) 309 | } 310 | 311 | func (s *BuildSession) debugLog(format string, a ...interface{}) { 312 | LogDebug(Sprintf("%v\n", format), a...) 313 | } 314 | -------------------------------------------------------------------------------- /agent/command_and.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | ) 22 | 23 | func CommandAnd(s *BuildSession, cmd *protocol.BuildCommand) error { 24 | for _, sub := range cmd.SubCommands { 25 | _, err := s.processTestCommand(sub) 26 | if err != nil { 27 | return err 28 | } 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /agent/command_cleandir.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "github.com/gocd-contrib/gocd-golang-agent/stream" 22 | "io" 23 | "io/ioutil" 24 | "os" 25 | "path/filepath" 26 | "strings" 27 | ) 28 | 29 | func CommandCleandir(s *BuildSession, cmd *protocol.BuildCommand) (err error) { 30 | path := cmd.Args["path"] 31 | allows, err := cmd.ListArg("allowed") 32 | if err != nil { 33 | return 34 | } 35 | fullPath := filepath.Join(s.wd, path) 36 | s.debugLog("cleandir %v, excludes: %+v", fullPath, allows) 37 | return Cleandir(s.console, fullPath, allows...) 38 | } 39 | 40 | func Cleandir(log io.Writer, root string, allows ...string) error { 41 | root = filepath.Clean(root) 42 | for i, allow := range allows { 43 | allows[i] = filepath.Clean(filepath.Join(root, allow)) 44 | if allows[i] == root { 45 | return nil 46 | } 47 | if strings.HasPrefix(root, allows[i]) { 48 | return Err("Cannot clean directory. Folder %v is outside the base folder %v", allows[i], root) 49 | } 50 | } 51 | w := stream.NewSubstituteWriter(log) 52 | w.Substitutions[" "+root+"/"] = " " 53 | return cleandir(w, root, allows...) 54 | } 55 | 56 | func cleandir(log io.Writer, root string, allows ...string) error { 57 | infos, err := ioutil.ReadDir(root) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | for _, finfo := range infos { 63 | fpath := filepath.Join(root, finfo.Name()) 64 | if finfo.IsDir() { 65 | match := "" 66 | for _, allow := range allows { 67 | if strings.HasPrefix(allow, fpath) { 68 | match = allow 69 | break 70 | } 71 | } 72 | if match == "" { 73 | log.Write([]byte(Sprintf("Deleting folder %v\n", fpath))) 74 | if err := os.RemoveAll(fpath); err != nil { 75 | return err 76 | } 77 | } else if fpath != match { 78 | if err := cleandir(log, fpath, allows...); err != nil { 79 | return err 80 | } 81 | } else { 82 | log.Write([]byte(Sprintf("Keeping folder %v\n", fpath))) 83 | } 84 | } else { 85 | log.Write([]byte(Sprintf("Deleting file %v\n", fpath))) 86 | err := os.Remove(fpath) 87 | if err != nil { 88 | return err 89 | } 90 | } 91 | } 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /agent/command_cleandir_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package agent_test 17 | 18 | import ( 19 | "bytes" 20 | "github.com/bmatcuk/doublestar" 21 | . "github.com/gocd-contrib/gocd-golang-agent/agent" 22 | "github.com/xli/assert" 23 | "io/ioutil" 24 | "path/filepath" 25 | "sort" 26 | "testing" 27 | ) 28 | 29 | func TestCleandir(t *testing.T) { 30 | tmpDir, err := ioutil.TempDir("", "cleandir-test") 31 | assert.Nil(t, err) 32 | createTestProject(tmpDir) 33 | 34 | var log bytes.Buffer 35 | err = Cleandir(&log, tmpDir, "src/hello", "test/world2") 36 | assert.Nil(t, err) 37 | 38 | matches, err := doublestar.Glob(filepath.Join(tmpDir, "**/*.txt")) 39 | assert.Nil(t, err) 40 | sort.Strings(matches) 41 | expected := []string{ 42 | "src/hello/3.txt", 43 | "src/hello/4.txt", 44 | "test/world2/10.txt", 45 | "test/world2/11.txt", 46 | } 47 | 48 | for i, f := range matches { 49 | actual := f[len(tmpDir)+1:] 50 | assert.Equal(t, expected[i], actual) 51 | } 52 | expectedLog := `Deleting file 0.txt 53 | Deleting file src/1.txt 54 | Deleting file src/2.txt 55 | Keeping folder src/hello 56 | Deleting file test/5.txt 57 | Deleting file test/6.txt 58 | Deleting file test/7.txt 59 | Deleting file test/world/10.txt 60 | Deleting file test/world/11.txt 61 | Deleting file test/world/8.txt 62 | Deleting file test/world/9.txt 63 | Keeping folder test/world2 64 | ` 65 | assert.Equal(t, expectedLog, log.String()) 66 | } 67 | 68 | func TestShouldFailWhenCleandirAllowsContainsPathThatIsOutsideOfBaseDir(t *testing.T) { 69 | tmpDir, err := ioutil.TempDir("", "cleandir-test2") 70 | assert.Nil(t, err) 71 | createTestProject(tmpDir) 72 | 73 | var log bytes.Buffer 74 | err = Cleandir(&log, tmpDir, "test/world2", "./../") 75 | assert.NotNil(t, err) 76 | assert.Equal(t, "", log.String()) 77 | } 78 | -------------------------------------------------------------------------------- /agent/command_compose.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | ) 22 | 23 | func CommandCompose(s *BuildSession, cmd *protocol.BuildCommand) error { 24 | var err error 25 | for _, sub := range cmd.SubCommands { 26 | if err != nil { 27 | s.process(sub) 28 | } else { 29 | err = s.process(sub) 30 | } 31 | } 32 | return err 33 | } 34 | -------------------------------------------------------------------------------- /agent/command_cond.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | ) 22 | 23 | func CommandCond(s *BuildSession, cmd *protocol.BuildCommand) error { 24 | for i := 0; i < len(cmd.SubCommands); i += 2 { 25 | if i == len(cmd.SubCommands)-1 { 26 | // else branch 27 | return s.process(cmd.SubCommands[i]) 28 | } 29 | test := cmd.SubCommands[i] 30 | action := cmd.SubCommands[i+1] 31 | _, err := s.processTestCommand(test) 32 | if err == nil { 33 | return s.process(action) 34 | } 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /agent/command_download_artifact.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "path/filepath" 22 | ) 23 | 24 | func CommandDownloadArtifact(s *BuildSession, cmd *protocol.BuildCommand) error { 25 | checksumURL, err := config.MakeFullServerURL(cmd.Args["checksumUrl"]) 26 | if err != nil { 27 | return err 28 | } 29 | absChecksumFile := filepath.Join(s.wd, cmd.Args["checksumFile"]) 30 | err = s.artifacts.DownloadFile(checksumURL, absChecksumFile) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | srcURL, err := config.MakeFullServerURL(cmd.Args["url"]) 36 | if err != nil { 37 | return err 38 | } 39 | srcPath := cmd.Args["src"] 40 | absDestPath := filepath.Join(s.wd, cmd.Args["dest"]) 41 | if cmd.Name == protocol.CommandDownloadDir { 42 | _, fname := filepath.Split(srcPath) 43 | absDestPath = filepath.Join(s.wd, cmd.Args["dest"], fname) 44 | } 45 | err = s.artifacts.VerifyChecksum(srcPath, absDestPath, absChecksumFile) 46 | if err == nil { 47 | s.ConsoleLog("[%v] exists and matches checksum, does not need dowload it from server.\n", srcPath) 48 | return nil 49 | } 50 | s.debugLog("download %v to %v", srcURL, absDestPath) 51 | if cmd.Name == protocol.CommandDownloadDir { 52 | err = s.artifacts.DownloadDir(srcURL, absDestPath) 53 | } else { 54 | err = s.artifacts.DownloadFile(srcURL, absDestPath) 55 | } 56 | if err != nil { 57 | return err 58 | } 59 | return s.artifacts.VerifyChecksum(srcPath, absDestPath, absChecksumFile) 60 | } 61 | -------------------------------------------------------------------------------- /agent/command_echo.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | ) 22 | 23 | func CommandEcho(s *BuildSession, cmd *protocol.BuildCommand) error { 24 | line := cmd.Args["line"] 25 | s.echo.Write([]byte(line)) 26 | s.echo.Write([]byte{'\n'}) 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /agent/command_exec.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "os/exec" 22 | "strings" 23 | ) 24 | 25 | func CommandExec(s *BuildSession, cmd *protocol.BuildCommand) error { 26 | args, err := cmd.ListArg("args") 27 | if err != nil { 28 | return err 29 | } 30 | execCmd := exec.Command(cmd.Args["command"], args...) 31 | execCmd.Env = s.Env() 32 | execCmd.Stdout = s.secrets 33 | execCmd.Stderr = s.secrets 34 | execCmd.Dir = s.wd 35 | execCmd.Stdin = strings.NewReader(cmd.ExecInput) 36 | done := make(chan error) 37 | if err := execCmd.Start(); err != nil { 38 | return err 39 | } 40 | go func() { 41 | done <- execCmd.Wait() 42 | }() 43 | 44 | select { 45 | case <-s.cancel: 46 | s.debugLog("received cancel signal") 47 | LogInfo("kill process(%v) %v", execCmd.Process, cmd.Args) 48 | if err := execCmd.Process.Kill(); err != nil { 49 | LogInfo("Kill command %v failed, error: %v\n", cmd.Args, err) 50 | } else { 51 | LogInfo("process %v is killed", execCmd.Process) 52 | } 53 | return Err("%v is canceled", cmd.Args) 54 | case err := <-done: 55 | return err 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /agent/command_export.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "os" 22 | ) 23 | 24 | func CommandExport(s *BuildSession, cmd *protocol.BuildCommand) error { 25 | msg := "setting environment variable '%v' to value '%v'\n" 26 | name := cmd.Args["name"] 27 | value, ok := cmd.Args["value"] 28 | if !ok { 29 | s.ConsoleLog(msg, name, os.Getenv(name)) 30 | return nil 31 | } 32 | secure := cmd.Args["secure"] 33 | displayValue := value 34 | if secure == "true" { 35 | displayValue = DefaultSecretMask 36 | } 37 | _, override := s.envs[name] 38 | if override || os.Getenv(name) != "" { 39 | msg = "overriding environment variable '%v' with value '%v'\n" 40 | } 41 | s.envs[name] = value 42 | s.ConsoleLog(msg, name, displayValue) 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /agent/command_fail.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | ) 22 | 23 | func CommandFail(s *BuildSession, cmd *protocol.BuildCommand) error { 24 | return Err(cmd.Args["message"]) 25 | } 26 | -------------------------------------------------------------------------------- /agent/command_generate_test_report.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/bmatcuk/doublestar" 21 | "github.com/gocd-contrib/gocd-golang-agent/junit" 22 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 23 | "html/template" 24 | "os" 25 | "path/filepath" 26 | "sort" 27 | "strings" 28 | "github.com/gocd-contrib/gocd-golang-agent/nunit" 29 | ) 30 | 31 | type UnitTestReport struct { 32 | Tests int 33 | Failures int 34 | Skipped int 35 | Time float64 36 | TestCases []*TestCase 37 | } 38 | 39 | type TestCase struct { 40 | Name string 41 | Failure *Failure 42 | Error *Error 43 | } 44 | 45 | type FailureMessage struct { 46 | StackTrace string 47 | } 48 | 49 | type Failure struct { 50 | FailureMessage 51 | } 52 | 53 | type Error struct { 54 | FailureMessage 55 | } 56 | 57 | func (r *UnitTestReport) Merge(another *UnitTestReport) { 58 | r.Tests += another.Tests 59 | r.Failures += another.Failures 60 | r.Skipped += another.Skipped 61 | r.Time += another.Time 62 | r.TestCases = append(r.TestCases, another.TestCases...) 63 | } 64 | 65 | func CommandGenerateTestReport(s *BuildSession, cmd *protocol.BuildCommand) error { 66 | srcs, err := cmd.ListArg("srcs") 67 | if err != nil { 68 | return err 69 | } 70 | if len(srcs) == 0 { 71 | return nil 72 | } 73 | uploadPath := cmd.Args["uploadPath"] 74 | 75 | report := new(UnitTestReport) 76 | 77 | junitRep, err := generateUnitTestReportFromJunitReport(s, srcs) 78 | if err != nil { 79 | return err 80 | } 81 | report.Merge(junitRep) 82 | 83 | nUnitRep, err := generateUnitTestReportFromNunitReport(s, srcs) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | report.Merge(nUnitRep) 89 | 90 | return uploadUnitTestReportArtifacts(s, uploadPath, report) 91 | } 92 | 93 | func uploadUnitTestReportArtifacts(s *BuildSession, uploadPath string, req *UnitTestReport) error { 94 | 95 | template, err := loadTestReportTemplate() 96 | if err != nil { 97 | return err 98 | } 99 | 100 | outputPath := filepath.Join(s.wd, uploadPath, protocol.TestReportFileName) 101 | 102 | err = Mkdirs(filepath.Dir(outputPath)) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | file, err := os.Create(outputPath) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | err = template.Execute(file, req) 113 | 114 | defer file.Close() 115 | 116 | if err != nil { 117 | return err 118 | } 119 | return uploadArtifacts(s, file.Name(), uploadPath, false) 120 | } 121 | 122 | func generateUnitTestReportFromNunitReport(s *BuildSession, srcs []string) (report *UnitTestReport, err error) { 123 | 124 | results := nunit.NewTestResults() 125 | report = new(UnitTestReport) 126 | 127 | for _, src := range srcs { 128 | path := filepath.Join(s.wd, src) 129 | if strings.Contains(path, "*") { 130 | matches, err1 := doublestar.Glob(path) 131 | if err1 != nil { 132 | err = err1 133 | } 134 | sort.Strings(matches) 135 | for _, fpath := range matches { 136 | generateNUnitTestReport(s, results, fpath) 137 | } 138 | } else { 139 | generateNUnitTestReport(s, results, path) 140 | } 141 | } 142 | 143 | s.debugLog("nunit test report: %+v", results) 144 | 145 | report.Tests = results.Total 146 | report.Skipped = results.Skipped 147 | report.Failures = results.Failures + results.Errors 148 | report.Time = results.Time 149 | 150 | report.TestCases = mapNunitTestCaseToTemplate(results.TestCases) 151 | 152 | return 153 | 154 | } 155 | 156 | func generateUnitTestReportFromJunitReport(s *BuildSession, srcs []string) (report *UnitTestReport, err error) { 157 | suite := junit.NewTestSuite() 158 | report = new(UnitTestReport) 159 | 160 | for _, src := range srcs { 161 | path := filepath.Join(s.wd, src) 162 | if strings.Contains(path, "*") { 163 | matches, err1 := doublestar.Glob(path) 164 | if err1 != nil { 165 | err = err1 166 | } 167 | sort.Strings(matches) 168 | for _, fpath := range matches { 169 | generateJunitTestReport(s, suite, fpath) 170 | } 171 | } else { 172 | generateJunitTestReport(s, suite, path) 173 | } 174 | } 175 | 176 | s.debugLog("test report: %+v", suite) 177 | 178 | report.Tests = suite.Tests 179 | report.Skipped = suite.Skipped 180 | report.Failures = suite.Failures + suite.Errors 181 | report.TestCases = mapJunitTestCaseToTemplate(suite.TestCases) 182 | report.Time = suite.Time 183 | 184 | return 185 | } 186 | 187 | func generateNUnitTestReport(s *BuildSession, result *nunit.TestResults, path string) { 188 | err := nunit.GenerateNUnitTestReport(result, path) 189 | if err != nil { 190 | s.debugLog("ignore %v for error: %v", path, err) 191 | return 192 | } 193 | return 194 | } 195 | 196 | func generateJunitTestReport(s *BuildSession, result *junit.TestSuite, path string) { 197 | err := junit.GenerateJunitTestReport(result, path) 198 | if err != nil { 199 | s.debugLog("ignore %v for error: %v", path, err) 200 | return 201 | } 202 | return 203 | } 204 | 205 | func mapJunitTestCaseToTemplate(testCases []*junit.TestCase) (results []*TestCase) { 206 | for _, item := range testCases { 207 | t := new(TestCase) 208 | t.Name = item.Name 209 | if item.Failure != nil { 210 | t.Failure = new(Failure) 211 | t.Failure.StackTrace = item.Failure.StackTrace 212 | } 213 | if item.Error != nil { 214 | t.Error = new(Error) 215 | t.Error.StackTrace = item.Error.StackTrace 216 | } 217 | results = append(results, t) 218 | } 219 | return 220 | } 221 | 222 | func mapNunitTestCaseToTemplate(testCases []*nunit.TestCase) (results []*TestCase) { 223 | for _, item := range testCases { 224 | t := new(TestCase) 225 | t.Name = item.Name 226 | if item.Failure != nil { 227 | t.Failure = new(Failure) 228 | t.Failure.StackTrace = item.Failure.StackTrace.Content 229 | } 230 | results = append(results, t) 231 | } 232 | return 233 | } 234 | 235 | func loadTestReportTemplate() (*template.Template, error) { 236 | return template.New("TestReportTemplate").Parse(UNIT_TEST_REPORT_TEMPLATE) 237 | } -------------------------------------------------------------------------------- /agent/command_mkdirs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "path/filepath" 22 | ) 23 | 24 | func CommandMkdirs(s *BuildSession, cmd *protocol.BuildCommand) error { 25 | path := cmd.Args["path"] 26 | fullPath := filepath.Join(s.wd, path) 27 | s.debugLog("mkdirs %v", fullPath) 28 | return Mkdirs(fullPath) 29 | } 30 | -------------------------------------------------------------------------------- /agent/command_or.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | ) 22 | 23 | func CommandOr(s *BuildSession, cmd *protocol.BuildCommand) error { 24 | var err error 25 | for _, sub := range cmd.SubCommands { 26 | _, te := s.processTestCommand(sub) 27 | if te == nil { 28 | return nil 29 | } else { 30 | err = te 31 | } 32 | } 33 | return err 34 | } 35 | -------------------------------------------------------------------------------- /agent/command_report.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | ) 22 | 23 | func CommandReport(s *BuildSession, cmd *protocol.BuildCommand) error { 24 | jobState := cmd.Args["status"] 25 | s.debugLog("report %v", jobState) 26 | s.send <- protocol.ReportMessage(cmd.Name, s.Report(jobState)) 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /agent/command_secret.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | ) 22 | 23 | func CommandSecret(s *BuildSession, cmd *protocol.BuildCommand) error { 24 | value := cmd.Args["value"] 25 | substitution := cmd.Args["substitution"] 26 | if substitution == "" { 27 | substitution = DefaultSecretMask 28 | } 29 | s.debugLog("%v => %v", value, substitution) 30 | s.secrets.Substitutions[value] = substitution 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /agent/command_upload_artifact.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/bmatcuk/doublestar" 21 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 22 | "os" 23 | "path/filepath" 24 | "sort" 25 | "strings" 26 | ) 27 | 28 | func CommandUploadArtifact(s *BuildSession, cmd *protocol.BuildCommand) error { 29 | src := cmd.Args["src"] 30 | destDir := cmd.Args["dest"] 31 | ignoreUnmatchError := cmd.Args["ignoreUnmatchError"] == "true" 32 | 33 | absSrc := filepath.Join(s.wd, src) 34 | return uploadArtifacts(s, absSrc, strings.Replace(destDir, "\\", "/", -1), ignoreUnmatchError) 35 | } 36 | 37 | func uploadArtifacts(s *BuildSession, source, destDir string, ignoreUnmatchError bool) (err error) { 38 | if strings.Contains(source, "*") { 39 | matches, err := doublestar.Glob(source) 40 | if err != nil { 41 | return err 42 | } 43 | sort.Strings(matches) 44 | base := BaseDirOfPathWithWildcard(source) 45 | baseLen := len(base) 46 | for _, file := range matches { 47 | fileDir, _ := filepath.Split(file) 48 | dest := Join("/", destDir, fileDir[baseLen:len(fileDir)-1]) 49 | err = uploadArtifacts(s, file, dest, ignoreUnmatchError) 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | srcInfo, err := os.Stat(source) 58 | if err != nil { 59 | if ignoreUnmatchError { 60 | return nil 61 | } 62 | return 63 | } 64 | s.ConsoleLog("Uploading artifacts from %v to %v\n", source, destDescription(destDir)) 65 | 66 | var destPath string 67 | if destDir != "" { 68 | destPath = Join("/", destDir, srcInfo.Name()) 69 | } else { 70 | destPath = srcInfo.Name() 71 | } 72 | destURL := AppendUrlParam(AppendUrlPath(s.artifactUploadBaseURL, destDir), 73 | "buildId", s.buildId) 74 | return s.artifacts.Upload(source, destPath, destURL) 75 | } 76 | 77 | func destDescription(path string) string { 78 | if path == "" { 79 | return "[defaultRoot]" 80 | } else { 81 | return path 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /agent/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "net" 21 | "net/url" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | "time" 26 | "crypto/tls" 27 | ) 28 | 29 | type Config struct { 30 | Hostname string 31 | SendMessageTimeout time.Duration 32 | ServerUrl *url.URL 33 | ServerHostAndPort string 34 | ContextPath string 35 | WebSocketPath string 36 | RegistrationPath string 37 | TokenPath string 38 | WorkingDir string 39 | LogDir string 40 | ConfigDir string 41 | IpAddress string 42 | 43 | AgentAutoRegisterKey string 44 | AgentAutoRegisterResources string 45 | AgentAutoRegisterEnvironments string 46 | AgentAutoRegisterElasticAgentId string 47 | AgentAutoRegisterElasticPluginId string 48 | 49 | GoServerCAFile string 50 | AgentPrivateKeyFile string 51 | AgentCertFile string 52 | AgentIdFile string 53 | AgentTokenFile string 54 | OutputDebugLog bool 55 | } 56 | 57 | func LoadConfig() *Config { 58 | gocdServerURL := readEnv("GOCD_SERVER_URL", "https://localhost:8154/go") 59 | os.Setenv("GO_SERVER_URL", gocdServerURL) 60 | serverUrl, err := url.Parse(gocdServerURL) 61 | if err != nil { 62 | panic(err) 63 | } 64 | serverUrl.Scheme = "https" 65 | hostname, _ := os.Hostname() 66 | wd, err := filepath.Abs(os.Getenv("GOCD_AGENT_WORKING_DIR")) 67 | if err != nil { 68 | panic(Sprintf("GOCD_AGENT_WORKING_DIR is invalid: %v", err)) 69 | } 70 | wd = filepath.Clean(wd) 71 | configDir := filepath.Join(wd, readEnv("GOCD_AGENT_CONFIG_DIR", "config")) 72 | return &Config{ 73 | Hostname: hostname, 74 | SendMessageTimeout: 120 * time.Second, 75 | ServerUrl: serverUrl, 76 | ServerHostAndPort: serverUrl.Host, 77 | WorkingDir: wd, 78 | LogDir: os.Getenv("GOCD_AGENT_LOG_DIR"), 79 | ConfigDir: configDir, 80 | GoServerCAFile: filepath.Join(configDir, "go-server-ca.pem"), 81 | AgentPrivateKeyFile: filepath.Join(configDir, "agent-private-key.pem"), 82 | AgentCertFile: filepath.Join(configDir, "agent-cert.pem"), 83 | AgentIdFile: filepath.Join(configDir, "agent-id"), 84 | AgentTokenFile: filepath.Join(configDir, "token"), 85 | AgentAutoRegisterKey: os.Getenv("GOCD_AGENT_AUTO_REGISTER_KEY"), 86 | AgentAutoRegisterResources: os.Getenv("GOCD_AGENT_AUTO_REGISTER_RESOURCES"), 87 | AgentAutoRegisterEnvironments: os.Getenv("GOCD_AGENT_AUTO_REGISTER_ENVIRONMENTS"), 88 | AgentAutoRegisterElasticAgentId: os.Getenv("GOCD_AGENT_AUTO_REGISTER_ELASTIC_AGENT_ID"), 89 | AgentAutoRegisterElasticPluginId: os.Getenv("GOCD_AGENT_AUTO_REGISTER_ELASTIC_PLUGIN_ID"), 90 | OutputDebugLog: os.Getenv("DEBUG") != "", 91 | WebSocketPath: readEnv("GOCD_SERVER_WEB_SOCKET_PATH", "/agent-websocket"), 92 | RegistrationPath: readEnv("GOCD_SERVER_REGISTRATION_PATH", "/admin/agent"), 93 | TokenPath: readEnv( "GOCD_SERVER_TOKEN_PATH", "/admin/agent/token"), 94 | IpAddress: lookupIpAddress(serverUrl.Host), 95 | } 96 | } 97 | 98 | func lookupIpAddress(host string) string { 99 | conn, err := tls.Dial("tcp", host, &tls.Config{ 100 | InsecureSkipVerify: true, 101 | }) 102 | if err != nil { 103 | return checkAllInterfaces() 104 | } 105 | ipAddress := strings.Split(conn.LocalAddr().String(), ":")[0] 106 | conn.Close() 107 | return ipAddress 108 | } 109 | 110 | func checkAllInterfaces() string { 111 | addrs, err := net.InterfaceAddrs() 112 | if err != nil { 113 | panic(err) 114 | } 115 | 116 | for _, a := range addrs { 117 | if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 118 | if ipnet.IP.To4() != nil { 119 | return ipnet.IP.String() 120 | } 121 | } 122 | } 123 | return "127.0.0.1" 124 | } 125 | 126 | func (c *Config) HttpsServerURL() string { 127 | return c.ServerUrl.String() 128 | } 129 | 130 | func (c *Config) WssServerURL() string { 131 | u, _ := url.Parse(c.HttpsServerURL()) 132 | u.Scheme = "wss" 133 | return Join("/", u.String(), c.WebSocketPath) 134 | } 135 | 136 | func (c *Config) RegistrationURL() (*url.URL, error) { 137 | return c.MakeFullServerURL(c.RegistrationPath) 138 | } 139 | 140 | func (c *Config) TokenURL(agentID string) (*url.URL, error) { 141 | return c.MakeFullServerURL(c.TokenPath + "?uuid=" + agentID) 142 | } 143 | 144 | func (c *Config) MakeFullServerURL(u string) (*url.URL, error) { 145 | if strings.HasPrefix(u, "/") { 146 | return url.Parse(Join("/", c.HttpsServerURL(), u)) 147 | } else { 148 | return url.Parse(u) 149 | } 150 | } 151 | 152 | func (c *Config) IsElasticAgent() bool { 153 | return config.AgentAutoRegisterElasticPluginId == "" 154 | } 155 | 156 | func readEnv(varname string, defaultVal string) string { 157 | val := os.Getenv(varname) 158 | if val == "" { 159 | return defaultVal 160 | } else { 161 | return val 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /agent/diskspace.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | /* 4 | * Copyright 2016 ThoughtWorks, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package agent 20 | 21 | import ( 22 | "strconv" 23 | "syscall" 24 | ) 25 | 26 | func UsableSpace() int64 { 27 | _, free, err := diskSpace("/") 28 | if err != nil { 29 | LogInfo("Unknown diskspace, error: %v", err) 30 | return -1 31 | } 32 | return free 33 | } 34 | 35 | func UsableSpaceString() string { 36 | return strconv.FormatInt(UsableSpace(), 10) 37 | } 38 | 39 | // Space returns total and free bytes available in a directory, e.g. `/`. 40 | // Think of it as "df" UNIX command. 41 | func diskSpace(path string) (total, free int64, err error) { 42 | s := syscall.Statfs_t{} 43 | err = syscall.Statfs(path, &s) 44 | if err != nil { 45 | return 46 | } 47 | total = int64(s.Bsize) * int64(s.Blocks) 48 | free = int64(s.Bsize) * int64(s.Bfree) 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /agent/diskspace_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | /* 4 | * Copyright 2016 ThoughtWorks, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package agent 20 | 21 | import ( 22 | "strconv" 23 | "syscall" 24 | "unsafe" 25 | "os" 26 | ) 27 | 28 | func UsableSpace() int64 { 29 | wd, err := os.Getwd() 30 | if err != nil { 31 | LogInfo("Cannot find working directory, error: %v", err) 32 | return -1 33 | } 34 | _, free, err := diskSpace(wd) 35 | if err != nil { 36 | LogInfo("Unknown diskspace, error: %v", err) 37 | return -1 38 | } 39 | return free 40 | } 41 | 42 | func UsableSpaceString() string { 43 | return strconv.FormatInt(UsableSpace(), 10) 44 | } 45 | 46 | 47 | func diskSpace(path string) (total, free int64, err error) { 48 | var ( 49 | freeBytes int64 50 | totalBytes int64 51 | availBytes int64 52 | ) 53 | 54 | h := syscall.MustLoadDLL("kernel32.dll") 55 | c := h.MustFindProc("GetDiskFreeSpaceExW") 56 | 57 | // According to MSDN, err will never be nil, r1 = 0 mean call failed 58 | r1, _, errCall := c.Call( 59 | uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))), 60 | uintptr(unsafe.Pointer(&freeBytes)), 61 | uintptr(unsafe.Pointer(&totalBytes)), 62 | uintptr(unsafe.Pointer(&availBytes))) 63 | if r1 != 0 { 64 | err = nil 65 | }else{ 66 | err = errCall 67 | } 68 | total = totalBytes 69 | free = freeBytes 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /agent/logging.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "io" 21 | "io/ioutil" 22 | "log" 23 | "os" 24 | "path/filepath" 25 | ) 26 | 27 | type Logger struct { 28 | Info *log.Logger 29 | Debug *log.Logger 30 | Error *log.Logger 31 | } 32 | 33 | func MakeLogger(logDir, file string, debug bool) *Logger { 34 | var output, debugOutput io.Writer 35 | if logDir != "" { 36 | fpath := filepath.Join(logDir, file) 37 | var err error 38 | output, err = os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 39 | if err != nil { 40 | panic(err) 41 | } 42 | } else { 43 | output = os.Stdout 44 | } 45 | 46 | if debug { 47 | debugOutput = output 48 | } else { 49 | debugOutput = ioutil.Discard 50 | } 51 | 52 | debugLogger := log.New(debugOutput, "", 0) 53 | infoLogger := log.New(output, "", 0) 54 | errorLogger := log.New(output, "ERROR: ", log.Lshortfile) 55 | 56 | return &Logger{Debug: debugLogger, Info: infoLogger, Error: errorLogger} 57 | } 58 | -------------------------------------------------------------------------------- /agent/not_implemented.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | ) 22 | 23 | func NotImplemented(s *BuildSession, cmd *protocol.BuildCommand) error { 24 | s.warn("Golang Agent does not support build comamnd '%v' yet, related GoCD feature will not be supported. More details: https://github.com/gocd-contrib/gocd-golang-agent", cmd.Name) 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /agent/on_cancel_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package agent_test 17 | 18 | import ( 19 | . "github.com/gocd-contrib/gocd-golang-agent/agent" 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "github.com/xli/assert" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func TestOnCancel1(t *testing.T) { 27 | setUp(t) 28 | defer tearDown() 29 | goServer.SendBuild(AgentId, buildId, 30 | protocol.ComposeCommand( 31 | echo("echo before sleep"), 32 | protocol.ExecCommand("sleep", "5").SetOnCancel(echo("read on cancel")), 33 | echo("should not process this echo").RunIf("any"), 34 | ).SetOnCancel(protocol.ExecCommand("echo", "compose on cancel")), 35 | echo("should not process this echo"), 36 | ) 37 | 38 | assert.Equal(t, "agent Building", stateLog.Next()) 39 | 40 | goServer.Send(AgentId, protocol.CancelMessage()) 41 | 42 | assert.Equal(t, "build Cancelled", stateLog.Next()) 43 | assert.Equal(t, "agent Idle", stateLog.Next()) 44 | 45 | log, err := goServer.ConsoleLog(buildId) 46 | assert.Nil(t, err) 47 | 48 | expected := `echo before sleep 49 | read on cancel 50 | compose on cancel 51 | ` 52 | assert.Equal(t, expected, trimTimestamp(log)) 53 | } 54 | 55 | func TestOnCancel2(t *testing.T) { 56 | CancelCommandTimeout = 10 * time.Millisecond 57 | defer func() { 58 | CancelCommandTimeout = DefaultCancelCommandTimeout 59 | }() 60 | 61 | setUp(t) 62 | defer tearDown() 63 | cancel := protocol.ExecCommand("sleep", "60") 64 | goServer.SendBuild(AgentId, buildId, 65 | protocol.ExecCommand("sleep", "5").SetOnCancel(cancel), 66 | ) 67 | 68 | assert.Equal(t, "agent Building", stateLog.Next()) 69 | 70 | goServer.Send(AgentId, protocol.CancelMessage()) 71 | 72 | assert.Equal(t, "build Cancelled", stateLog.Next()) 73 | assert.Equal(t, "agent Idle", stateLog.Next()) 74 | 75 | log, err := goServer.ConsoleLog(buildId) 76 | assert.Nil(t, err) 77 | 78 | expected := `WARN: Kill cancel task because it did not finish in 10ms. 79 | ` 80 | assert.Equal(t, expected, trimTimestamp(log)) 81 | } 82 | 83 | func TestOnCancelShouldContinueMaskEchos(t *testing.T) { 84 | setUp(t) 85 | defer tearDown() 86 | goServer.SendBuild(AgentId, buildId, 87 | protocol.SecretCommand("secret", "$$$"), 88 | protocol.ExecCommand("sleep", "5").SetOnCancel(echo("secret on cancel: ${agent.location}")), 89 | ) 90 | 91 | assert.Equal(t, "agent Building", stateLog.Next()) 92 | 93 | goServer.Send(AgentId, protocol.CancelMessage()) 94 | 95 | assert.Equal(t, "build Cancelled", stateLog.Next()) 96 | assert.Equal(t, "agent Idle", stateLog.Next()) 97 | 98 | log, err := goServer.ConsoleLog(buildId) 99 | assert.Nil(t, err) 100 | 101 | config := GetConfig() 102 | expected := Sprintf("$$$ on cancel: %v\n", config.WorkingDir) 103 | assert.Equal(t, expected, trimTimestamp(log)) 104 | } 105 | 106 | func TestCancelBuildWhenBuildIsHangingOnTestCommand(t *testing.T) { 107 | setUp(t) 108 | defer tearDown() 109 | 110 | testCmd := protocol.ExecCommand("sleep", "5") 111 | goServer.SendBuild(AgentId, buildId, 112 | echo("hello before cancel"), 113 | echo("hello after sleep 5").SetTest(testCmd), 114 | ) 115 | assert.Equal(t, "agent Building", stateLog.Next()) 116 | 117 | goServer.Send(AgentId, protocol.CancelMessage()) 118 | 119 | assert.Equal(t, "build Cancelled", stateLog.Next()) 120 | assert.Equal(t, "agent Idle", stateLog.Next()) 121 | 122 | log, err := goServer.ConsoleLog(buildId) 123 | assert.Nil(t, err) 124 | 125 | expected := "hello before cancel\n" 126 | assert.Equal(t, expected, trimTimestamp(log)) 127 | } 128 | -------------------------------------------------------------------------------- /agent/registration.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "crypto/tls" 21 | "crypto/x509" 22 | "encoding/json" 23 | "encoding/pem" 24 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 25 | "io/ioutil" 26 | "net/http" 27 | "net/url" 28 | "os" 29 | "runtime" 30 | ) 31 | 32 | func ReadGoServerCACert() error { 33 | _, err := os.Stat(config.GoServerCAFile) 34 | if err == nil { 35 | return nil 36 | } 37 | 38 | LogInfo("fetching Go server[%v] CA certificate", config.ServerHostAndPort) 39 | conn, err := tls.Dial("tcp", config.ServerHostAndPort, &tls.Config{ 40 | InsecureSkipVerify: true, 41 | }) 42 | if err != nil { 43 | logger.Error.Printf("failed to connect: " + err.Error()) 44 | return err 45 | } 46 | defer conn.Close() 47 | state := conn.ConnectionState() 48 | certOut, err := os.Create(config.GoServerCAFile) 49 | if err != nil { 50 | logger.Error.Printf("failed to open %v for writing: %s", config.GoServerCAFile, err) 51 | return err 52 | } 53 | defer certOut.Close() 54 | for i:=0; i < len(state.PeerCertificates); i++ { 55 | pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: state.PeerCertificates[i].Raw}) 56 | } 57 | return nil 58 | } 59 | 60 | func GoServerRootCAs() (*x509.CertPool, error) { 61 | caCert, err := ioutil.ReadFile(config.GoServerCAFile) 62 | if err != nil { 63 | return nil, err 64 | } 65 | roots := x509.NewCertPool() 66 | ok := roots.AppendCertsFromPEM([]byte(caCert)) 67 | if !ok { 68 | return nil, Err("failed to parse root certificate") 69 | } 70 | return roots, nil 71 | } 72 | 73 | func GoServerTlsConfig(withClientCert bool) (*tls.Config, error) { 74 | certs := make([]tls.Certificate, 0) 75 | if withClientCert { 76 | cert, err := tls.LoadX509KeyPair(config.AgentCertFile, config.AgentPrivateKeyFile) 77 | if err != nil { 78 | return nil, err 79 | } 80 | certs = append(certs, cert) 81 | } 82 | roots, err := GoServerRootCAs() 83 | if err != nil { 84 | return nil, err 85 | } 86 | serverName, err := extractServerDN(config.GoServerCAFile) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return &tls.Config{ 91 | Certificates: certs, 92 | RootCAs: roots, 93 | ServerName: serverName, 94 | }, nil 95 | } 96 | 97 | func GoServerRemoteClient(withClientCert bool) (*http.Client, error) { 98 | config, err := GoServerTlsConfig(withClientCert) 99 | if err != nil { 100 | return nil, err 101 | } 102 | tr := &http.Transport{ 103 | TLSClientConfig: config, 104 | } 105 | return &http.Client{Transport: tr}, nil 106 | } 107 | 108 | func Register() error { 109 | if err := ReadGoServerCACert(); err != nil { 110 | return err 111 | } 112 | if err := requestToken(); err != nil { 113 | return err 114 | } 115 | if err := readAgentKeyAndCerts(registerData()); err != nil { 116 | return err 117 | } 118 | return nil 119 | } 120 | 121 | func CleanRegistration() error { 122 | files := []string{config.GoServerCAFile, 123 | config.AgentPrivateKeyFile, 124 | config.AgentCertFile} 125 | for _, f := range files { 126 | _, err := os.Stat(f) 127 | if err == nil { 128 | err := os.Remove(f) 129 | if err != nil { 130 | return err 131 | } 132 | } 133 | } 134 | return nil 135 | } 136 | 137 | 138 | func requestToken() error { 139 | _, agentTokenErr := os.Stat(config.AgentTokenFile) 140 | if agentTokenErr == nil { 141 | return nil 142 | } 143 | 144 | client, err := GoServerRemoteClient(false) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | url, err := config.TokenURL(AgentId) 150 | if agentTokenErr != nil { 151 | LogInfo( "fetching token from : %v", url.String()) 152 | } 153 | resp, err := client.Get(url.String()) 154 | 155 | if err != nil { 156 | return err 157 | } 158 | defer resp.Body.Close() 159 | if resp.StatusCode == http.StatusOK { 160 | bodyBytes, err2 := ioutil.ReadAll(resp.Body) 161 | if err2 == nil { 162 | ioutil.WriteFile(config.AgentTokenFile, []byte(string(bodyBytes)), 0600) 163 | }else{ 164 | LogInfo("Token fetched but cannot read body") 165 | return err2 166 | } 167 | }else{ 168 | LogInfo("Cannot fetch token from : %v", url) 169 | return err 170 | } 171 | 172 | return nil 173 | } 174 | 175 | func registerData() map[string]string { 176 | return map[string]string{ 177 | "hostname": config.Hostname, 178 | "uuid": AgentId, 179 | "location": config.WorkingDir, 180 | "operatingSystem": runtime.GOOS, 181 | "usablespace": UsableSpaceString(), 182 | "agentAutoRegisterKey": config.AgentAutoRegisterKey, 183 | "agentAutoRegisterResources": config.AgentAutoRegisterResources, 184 | "agentAutoRegisterEnvironments": config.AgentAutoRegisterEnvironments, 185 | "agentAutoRegisterHostname": config.Hostname, 186 | "elasticAgentId": config.AgentAutoRegisterElasticAgentId, 187 | "elasticPluginId": config.AgentAutoRegisterElasticPluginId, 188 | "supportsBuildCommandProtocol": "true", 189 | } 190 | } 191 | 192 | func readAgentKeyAndCerts(params map[string]string) error { 193 | var token string 194 | _, agentPrivateKeyFileErr := os.Stat(config.AgentPrivateKeyFile) 195 | _, agentCertFileErr := os.Stat(config.AgentCertFile) 196 | _, agentTokenFileErr := os.Stat(config.AgentTokenFile) 197 | if agentPrivateKeyFileErr == nil && agentCertFileErr == nil && agentTokenFileErr == nil { 198 | return nil 199 | } 200 | 201 | client, err := GoServerRemoteClient(false) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | 207 | if _, err := os.Stat(config.AgentTokenFile); err == nil { 208 | data, err2 := ioutil.ReadFile(config.AgentTokenFile) 209 | if err2 != nil { 210 | logger.Error.Printf("failed to read token file(%v): %v", config.AgentTokenFile, err2) 211 | return err2 212 | } else { 213 | token = string(data) 214 | } 215 | } 216 | form := url.Values{} 217 | for k, v := range params { 218 | form.Add(k, v) 219 | } 220 | form.Add("token",token) 221 | 222 | 223 | 224 | url, err := config.RegistrationURL() 225 | LogInfo("fetching agent key and certificates from: %v", url) 226 | if err != nil { 227 | return err 228 | } 229 | resp, err := client.PostForm(url.String(), form) 230 | if err != nil { 231 | return err 232 | } 233 | 234 | defer resp.Body.Close() 235 | var registration protocol.Registration 236 | 237 | dec := json.NewDecoder(resp.Body) 238 | 239 | if err := dec.Decode(®istration); err != nil { 240 | return err 241 | } 242 | if registration.AgentCertificate == "" { 243 | return Err("Register failed, probably need approve agent registration on Server side") 244 | } 245 | 246 | ioutil.WriteFile(config.AgentPrivateKeyFile, []byte(registration.AgentPrivateKey), 0600) 247 | ioutil.WriteFile(config.AgentCertFile, []byte(registration.AgentCertificate), 0600) 248 | return nil 249 | } 250 | 251 | func extractServerDN(certFileName string) (string, error) { 252 | pemBlock, err := ioutil.ReadFile(certFileName) 253 | if err != nil { 254 | return "", err 255 | } 256 | 257 | der, _ := pem.Decode(pemBlock) 258 | cert, err := x509.ParseCertificate(der.Bytes) 259 | if err != nil { 260 | return "", err 261 | } 262 | return cert.Subject.CommonName, nil 263 | } 264 | -------------------------------------------------------------------------------- /agent/run_if_config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package agent_test 17 | 18 | import ( 19 | . "github.com/gocd-contrib/gocd-golang-agent/agent" 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "github.com/xli/assert" 22 | "testing" 23 | ) 24 | 25 | func TestRunIfConfig(t *testing.T) { 26 | setUp(t) 27 | defer tearDown() 28 | 29 | goServer.SendBuild(AgentId, buildId, 30 | protocol.EchoCommand("should not echo if failed when passed").RunIf("failed"), 31 | protocol.EchoCommand("should echo if any when passed").RunIf("any"), 32 | protocol.EchoCommand("should echo if passed when passed").RunIf("passed"), 33 | protocol.ExecCommand("cmdnotexist"), 34 | protocol.EchoCommand("should echo if failed when failed").RunIf("failed"), 35 | protocol.EchoCommand("should echo if any when failed").RunIf("any"), 36 | protocol.EchoCommand("should not echo if passed when failed").RunIf("passed"), 37 | ) 38 | 39 | assert.Equal(t, "agent Building", stateLog.Next()) 40 | assert.Equal(t, "build Failed", stateLog.Next()) 41 | assert.Equal(t, "agent Idle", stateLog.Next()) 42 | 43 | log, err := goServer.ConsoleLog(buildId) 44 | assert.Nil(t, err) 45 | 46 | expected := `should echo if any when passed 47 | should echo if passed when passed 48 | ERROR: exec: "cmdnotexist": executable file not found in $PATH 49 | should echo if failed when failed 50 | should echo if any when failed 51 | ` 52 | assert.Equal(t, expected, trimTimestamp(log)) 53 | } 54 | 55 | func TestComposeCommandWithRunIfConfig(t *testing.T) { 56 | setUp(t) 57 | defer tearDown() 58 | 59 | goServer.SendBuild(AgentId, buildId, 60 | protocol.ComposeCommand( 61 | protocol.ComposeCommand( 62 | protocol.EchoCommand("hello world1"), 63 | protocol.EchoCommand("hello world2"), 64 | ).RunIf("any"), 65 | protocol.ComposeCommand( 66 | protocol.EchoCommand("hello world3"), 67 | protocol.EchoCommand("hello world4"), 68 | ), 69 | ).RunIf("failed"), 70 | protocol.ComposeCommand( 71 | protocol.EchoCommand("hello world5").RunIf("failed"), 72 | protocol.EchoCommand("hello world6"), 73 | ), 74 | ) 75 | 76 | assert.Equal(t, "agent Building", stateLog.Next()) 77 | assert.Equal(t, "build Passed", stateLog.Next()) 78 | assert.Equal(t, "agent Idle", stateLog.Next()) 79 | 80 | log, err := goServer.ConsoleLog(buildId) 81 | assert.Nil(t, err) 82 | assert.Equal(t, "hello world6\n", trimTimestamp(log)) 83 | } 84 | -------------------------------------------------------------------------------- /agent/states.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "runtime" 22 | "sync" 23 | ) 24 | 25 | var state = map[string]string{ 26 | "runtimeStatus": "Idle", 27 | } 28 | 29 | var lock sync.Mutex 30 | 31 | func SetState(key, value string) { 32 | lock.Lock() 33 | defer lock.Unlock() 34 | LogInfo("set %v to %v", key, value) 35 | state[key] = value 36 | } 37 | 38 | func GetState(key string) string { 39 | lock.Lock() 40 | defer lock.Unlock() 41 | return state[key] 42 | } 43 | 44 | func GetAgentRuntimeInfo() *protocol.AgentRuntimeInfo { 45 | info := protocol.AgentRuntimeInfo{ 46 | Identifier: &protocol.AgentIdentifier{ 47 | HostName: config.Hostname, 48 | IpAddress: config.IpAddress, 49 | Uuid: AgentId, 50 | }, 51 | BuildingInfo: &protocol.AgentBuildingInfo{ 52 | BuildingInfo: GetState("buildLocatorForDisplay"), 53 | BuildLocator: GetState("buildLocator"), 54 | }, 55 | RuntimeStatus: GetState("runtimeStatus"), 56 | Location: config.WorkingDir, 57 | UsableSpace: UsableSpace(), 58 | OperatingSystemName: runtime.GOOS, 59 | ElasticPluginId: config.AgentAutoRegisterElasticPluginId, 60 | ElasticAgentId: config.AgentAutoRegisterElasticAgentId, 61 | SupportsBuildCommandProtocol: true, 62 | } 63 | if cookie := GetState("cookie"); cookie != "" { 64 | info.Cookie = cookie 65 | } 66 | return &info 67 | } 68 | -------------------------------------------------------------------------------- /agent/test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | ) 25 | 26 | func CommandTest(s *BuildSession, cmd *protocol.BuildCommand) error { 27 | flag := cmd.Args["flag"] 28 | 29 | if flag == "-eq" || flag == "-neq" || flag == "-in" || flag == "-nin" { 30 | output, err := s.processTestCommand(cmd.SubCommands[0]) 31 | if err != nil { 32 | s.debugLog("test -eq exec command error: %v", err) 33 | } 34 | expected := strings.TrimSpace(cmd.Args["left"]) 35 | actual := strings.TrimSpace(output.String()) 36 | 37 | if flag == "-eq" { 38 | if expected != actual { 39 | return Err("expected '%v', but was '%v'", expected, actual) 40 | } 41 | } else if flag == "-neq" { 42 | if expected == actual { 43 | return Err("expected different with '%v'", expected) 44 | } 45 | } else if flag == "-in" { 46 | if !strings.Contains(expected, actual) { 47 | return Err("expected command to contain '%v'", expected) 48 | } 49 | } else if flag == "-nin" { 50 | if strings.Contains(expected, actual) { 51 | return Err("expected command to not contain '%v'", expected) 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | targetPath := filepath.Join(s.wd, cmd.Args["left"]) 58 | info, err := os.Stat(targetPath) 59 | switch flag { 60 | case "-d": 61 | if err != nil { 62 | return err 63 | } 64 | if info.IsDir() { 65 | return nil 66 | } else { 67 | return Err("%v is not a directory", targetPath) 68 | } 69 | case "-nd": 70 | if err != nil { 71 | if os.IsNotExist(err) { 72 | return nil 73 | } 74 | return err 75 | } 76 | if info.IsDir() { 77 | return Err("%v is a directory", targetPath) 78 | } else { 79 | return nil 80 | } 81 | case "-f": 82 | if err != nil { 83 | return err 84 | } 85 | if info.IsDir() { 86 | return Err("%v is not a file", targetPath) 87 | } else { 88 | return nil 89 | } 90 | case "-nf": 91 | if err != nil { 92 | if os.IsNotExist(err) { 93 | return nil 94 | } 95 | return err 96 | } 97 | if info.IsDir() { 98 | return nil 99 | } else { 100 | return Err("%v is a file", targetPath) 101 | } 102 | } 103 | 104 | return Err("unknown test flag: %v", flag) 105 | } 106 | -------------------------------------------------------------------------------- /agent/unit_test_report_template.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | var UNIT_TEST_REPORT_TEMPLATE = `
20 |

Tests run: 21 | {{.Tests}} 22 | , Failures: 23 | {{.Failures}} 24 | , Not run: 25 | {{.Skipped}} 26 | , Time: 27 | {{.Time}} 28 | seconds. 29 |

30 |
31 | 32 | {{if .Failures }} 33 | 34 | {{range .TestCases}} 35 | {{if .Failure}} 36 | 37 | 38 | 39 | 40 | {{end}} 41 | {{if .Error}} 42 | 43 | 44 | 45 | 46 | {{end}} 47 | {{end}} 48 |
Failure{{ .Name }}
Error{{ .Name }}
49 | {{end}} 50 | {{if .Failures}} 51 | 52 | 53 | 54 | 55 | {{range .TestCases}} 56 | {{if .Failure}} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | {{end}} 70 | {{if .Error}} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | {{end}} 84 | {{end}} 85 |
Unit Test Failure and Error Details ({{ .Failures }})
Test:{{ .Name }}
Type:Failure
Message:{{ .Failure.StackTrace }}
Test:{{ .Name }}
Type:Error
Message:{{ .Error.StackTrace }}
86 | {{end}} 87 | ` 88 | -------------------------------------------------------------------------------- /agent/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "bytes" 21 | "crypto/md5" 22 | "errors" 23 | "fmt" 24 | "io" 25 | "net/url" 26 | "os" 27 | "path/filepath" 28 | "strings" 29 | "time" 30 | ) 31 | 32 | func Join(sep string, parts ...string) string { 33 | var buf bytes.Buffer 34 | for i, s := range parts { 35 | if len(s) == 0 || s == sep { 36 | if i < len(parts)-1 { 37 | buf.WriteString(sep) 38 | } 39 | continue 40 | } 41 | start := 0 42 | end := len(s) 43 | if i > 0 && s[0] == '/' { 44 | start++ 45 | } 46 | if i < len(parts) && s[len(s)-1] == '/' { 47 | end-- 48 | } 49 | buf.WriteString(s[start:end]) 50 | if i < len(parts)-1 { 51 | buf.WriteString(sep) 52 | } 53 | } 54 | return buf.String() 55 | } 56 | 57 | func BaseDirOfPathWithWildcard(path string) string { 58 | dir := strings.Split(path, "*")[0] 59 | if dir == "" { 60 | return "" 61 | } 62 | // clean up filename in the path 63 | base, _ := filepath.Split(dir) 64 | if base == "" { 65 | return "" 66 | } 67 | return base[:len(base)-1] 68 | } 69 | 70 | func AppendUrlParam(base *url.URL, paramName, paramValue string) *url.URL { 71 | url, _ := url.Parse(base.String()) 72 | values := url.Query() 73 | values.Set(paramName, paramValue) 74 | base.RawQuery = values.Encode() 75 | return url 76 | } 77 | 78 | func AppendUrlPath(base *url.URL, path string) *url.URL { 79 | url, _ := url.Parse(base.String()) 80 | url.RawPath = Join("/", url.RawPath, path) 81 | return url 82 | } 83 | 84 | func Mkdirs(path string) error { 85 | return os.MkdirAll(path, 0755) 86 | } 87 | 88 | func Sprintf(f string, args ...interface{}) string { 89 | return fmt.Sprintf(f, args...) 90 | } 91 | 92 | func Err(f string, args ...interface{}) error { 93 | return errors.New(Sprintf(f, args...)) 94 | } 95 | 96 | func closeAndWait(stop, closed chan bool, timeout time.Duration) error { 97 | select { 98 | case _, ok := <-stop: 99 | if !ok { 100 | return nil 101 | } 102 | default: 103 | } 104 | 105 | close(stop) 106 | 107 | select { 108 | case <-closed: 109 | return nil 110 | case <-time.After(timeout): 111 | return Err("Wait for closed timeout") 112 | } 113 | } 114 | 115 | func isClosedChan(ch chan bool) bool { 116 | select { 117 | case _, ok := <-ch: 118 | return !ok 119 | default: 120 | return false 121 | } 122 | } 123 | 124 | func ParseChecksum(checksum string) map[string]string { 125 | ret := make(map[string]string) 126 | for _, l := range strings.Split(checksum, "\n") { 127 | if strings.HasPrefix(l, "#") { 128 | continue 129 | } 130 | // issue #30 : The string 'l' sometimes has "\r" add which make the md5 checksum invalid. 131 | l = strings.Trim(l,"\r"); 132 | i := strings.Index(l, "=") 133 | if i > -1 { 134 | ret[l[:i]] = l[i+1:] 135 | } 136 | } 137 | return ret 138 | } 139 | 140 | func ComputeMd5(filePath string) (string, error) { 141 | file, err := os.Open(filePath) 142 | if err != nil { 143 | return "", err 144 | } 145 | defer file.Close() 146 | 147 | hash := md5.New() 148 | if _, err := io.Copy(hash, file); err != nil { 149 | return "", err 150 | } 151 | 152 | var result []byte 153 | return Sprintf("%x", hash.Sum(result)), nil 154 | } 155 | -------------------------------------------------------------------------------- /agent/utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package agent_test 17 | 18 | import ( 19 | . "github.com/gocd-contrib/gocd-golang-agent/agent" 20 | "github.com/xli/assert" 21 | "testing" 22 | ) 23 | 24 | func TestBaseDirOfPathWithWildcard(t *testing.T) { 25 | assert.Equal(t, "", BaseDirOfPathWithWildcard("/*")) 26 | assert.Equal(t, "", BaseDirOfPathWithWildcard("/**/*")) 27 | assert.Equal(t, "", BaseDirOfPathWithWildcard("/*.go")) 28 | 29 | assert.Equal(t, "/hello", BaseDirOfPathWithWildcard("/hello/*.go")) 30 | assert.Equal(t, "/hello/world", BaseDirOfPathWithWildcard("/hello/world/*.go")) 31 | assert.Equal(t, "/hello/world", BaseDirOfPathWithWildcard("/hello/world/**/*.go")) 32 | assert.Equal(t, "/hello/world", BaseDirOfPathWithWildcard("/hello/world/f*/*.go")) 33 | } 34 | 35 | func TestJoin(t *testing.T) { 36 | assert.Equal(t, "/", Join("/", "", "")) 37 | assert.Equal(t, "/", Join("/", "/", "/")) 38 | assert.Equal(t, "a/b", Join("/", "a", "b")) 39 | assert.Equal(t, "a/b", Join("/", "a", "/b")) 40 | assert.Equal(t, "a/b", Join("/", "a/", "/b")) 41 | assert.Equal(t, "a/", Join("/", "a/", "/")) 42 | assert.Equal(t, "a/b/", Join("/", "a/b", "/")) 43 | } 44 | 45 | func TestParseChecksum(t *testing.T) { 46 | checksum := `dest/3.txt=md5-3.txt 47 | 5.txt=md5-5.txt 48 | dest/world/10.txt=md5-10.txt 49 | dest/world2/10.txt=md5-10.2.txt 50 | dest/world=md5-world 51 | ` 52 | ret := ParseChecksum(checksum) 53 | assert.Equal(t, 5, len(ret)) 54 | assert.Equal(t, "md5-10.txt", ret["dest/world/10.txt"]) 55 | assert.Equal(t, "md5-5.txt", ret["5.txt"]) 56 | assert.Equal(t, "md5-world", ret["dest/world"]) 57 | } 58 | -------------------------------------------------------------------------------- /agent/websocket.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package agent 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "golang.org/x/net/websocket" 22 | "time" 23 | ) 24 | 25 | type WebsocketConnection struct { 26 | Conn *websocket.Conn 27 | Send chan *protocol.Message 28 | Received chan *protocol.Message 29 | } 30 | 31 | func (wc *WebsocketConnection) Close() { 32 | close(wc.Send) 33 | err := wc.Conn.Close() 34 | if err != nil { 35 | logger.Error.Printf("Close websocket connection failed: %v", err) 36 | } 37 | } 38 | 39 | func MakeWebsocketConnection(wsLoc, httpLoc string) (*WebsocketConnection, error) { 40 | tlsConfig, err := GoServerTlsConfig(true) 41 | if err != nil { 42 | return nil, err 43 | } 44 | wsConfig, err := websocket.NewConfig(wsLoc, httpLoc) 45 | if err != nil { 46 | return nil, err 47 | } 48 | wsConfig.TlsConfig = tlsConfig 49 | LogInfo("connect to: %v", wsLoc) 50 | ws, err := websocket.DialConfig(wsConfig) 51 | if err != nil { 52 | return nil, err 53 | } 54 | acknowledge := make(chan string) 55 | send := make(chan *protocol.Message) 56 | received := make(chan *protocol.Message) 57 | 58 | go startReceiveMessage(ws, received, acknowledge) 59 | go startSendMessage(ws, send, acknowledge) 60 | return &WebsocketConnection{Conn: ws, Send: send, Received: received}, nil 61 | } 62 | 63 | func startSendMessage(ws *websocket.Conn, send chan *protocol.Message, acknowledge chan string) { 64 | defer LogDebug("! exit goroutine: send message") 65 | connClosed := false 66 | loop: 67 | select { 68 | case id := <-acknowledge: 69 | LogInfo("Ignore acknowledge with id: %v", id) 70 | case msg, ok := <-send: 71 | if !ok { 72 | return 73 | } 74 | LogInfo("--> %v", msg.Action) 75 | if connClosed { 76 | logger.Error.Printf("send message failed: connection is closed") 77 | goto loop 78 | } 79 | if err := protocol.SendMessage(ws, msg); err == nil { 80 | waitForMessageAcknowledge(msg.AcknowledgeId, acknowledge) 81 | goto loop 82 | } else { 83 | logger.Error.Printf("send message failed: %v", err) 84 | if err := ws.Close(); err == nil { 85 | connClosed = true 86 | } else { 87 | logger.Error.Printf("Close websocket connection failed: %v", err) 88 | } 89 | } 90 | } 91 | goto loop 92 | } 93 | 94 | func waitForMessageAcknowledge(acknowledgeId string, acknowledge chan string) { 95 | for { 96 | select { 97 | case <-time.After(config.SendMessageTimeout): 98 | LogInfo("wait for message acknowledge timeout, id: %v", acknowledgeId) 99 | return 100 | case id := <-acknowledge: 101 | if id == acknowledgeId { 102 | return 103 | } else { 104 | LogInfo("ignore acknowledge with id: %v, expected: %v", id, acknowledgeId) 105 | } 106 | } 107 | } 108 | } 109 | 110 | func startReceiveMessage(ws *websocket.Conn, received chan *protocol.Message, acknowledge chan string) { 111 | defer LogDebug("! exit goroutine: receive message") 112 | defer close(received) 113 | for { 114 | msg, err := protocol.ReceiveMessage(ws) 115 | if err != nil { 116 | logger.Error.Printf("receive message failed: %v", err) 117 | return 118 | } 119 | LogInfo("<-- %v", msg.Action) 120 | 121 | if msg.Action == "acknowledge" { 122 | acknowledge <- msg.DataString() 123 | } else { 124 | received <- msg 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /build-agent.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -t golang-agent -f Dockerfile.golang-agent . 4 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | set +x 19 | set -e 20 | 21 | # src/github.com/gocd-contrib/gocd-golang-agent 22 | export GOPATH=`pwd`/../../../../ 23 | go get golang.org/x/net/websocket 24 | go get github.com/satori/go.uuid 25 | go get github.com/xli/assert 26 | go get github.com/bmatcuk/doublestar 27 | go get github.com/jstemmer/go-junit-report 28 | # go get -u all 29 | go test -test.v ./... | $GOPATH/bin/go-junit-report > testreport.xml 30 | -------------------------------------------------------------------------------- /build_gocd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License.. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | set +x 19 | set -e 20 | 21 | if [[ -f var ]]; then 22 | . vars 23 | fi 24 | 25 | BUILD_DIR=$(pwd) 26 | export GOPATH=$BUILD_DIR 27 | export GOBIN=$GOPATH/bin 28 | 29 | # 30 | # Pull dependencies 31 | # 32 | echo "================" 33 | echo "Get dependencies" 34 | echo "================" 35 | echo "" 36 | 37 | echo "------------------------------" 38 | echo "Get golang.org/x/net/websocket" 39 | go get -u golang.org/x/net/websocket 40 | 41 | echo "---------------------" 42 | echo "Get golang.org/x/text" 43 | go get -u golang.org/x/text 44 | 45 | echo "---------------------------" 46 | echo "Get golang.org/x/crypto/ssh" 47 | go get -u golang.org/x/crypto/ssh 48 | 49 | echo "---------------------------" 50 | echo "Get github.com/satori/go.uuid" 51 | go get -u github.com/satori/go.uuid 52 | 53 | echo "---------------------------" 54 | echo "Get github.com/xli/assert" 55 | go get -u github.com/xli/assert 56 | 57 | echo "---------------------------" 58 | echo "Get github.com/bmatcuk/doublestar" 59 | go get -u github.com/bmatcuk/doublestar 60 | 61 | # 62 | # Running Test and generate Test Report 63 | # 64 | echo "---------------------------" 65 | echo "Get github.com/jstemmer/go-junit-report" 66 | if [[ -f $GOPATH/bin/go-junit-report ]]; then 67 | echo "Remove binary : $GOPATH/bin/go-junit-report" 68 | rm -rf $GOPATH/bin/go-junit-report 69 | fi 70 | go get -u github.com/jstemmer/go-junit-report 71 | 72 | if [[ -f testreport.xml ]]; then 73 | echo "Remove old test report : testreport.xml" 74 | /bin/rm -rf testreport.xml 75 | fi 76 | go test -test.v github.com/gocd-contrib/gocd-golang-agent... | $GOPATH/bin/go-junit-report > testreport.xml 77 | 78 | # Go Build !! 79 | /bin/rm -rf output 80 | /bin/mkdir output 81 | echo "====================" 82 | echo "Start building gocd-golang-agent for linux" 83 | CGO_ENABLED=0 GOOS=linux go build -a -o output/gocd-golang-agent_linux_x86 github.com/gocd-contrib/gocd-golang-agent 84 | echo "====================" 85 | echo "Start building gocd-golang-agent for OSX" 86 | CGO_ENABLED=0 GOOS=darwin go build -a -o output/gocd-golang-agent_darwin_x86 github.com/gocd-contrib/gocd-golang-agent 87 | -------------------------------------------------------------------------------- /installers/deb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:trusty 2 | MAINTAINER GoCD Team 3 | 4 | RUN apt-get update 5 | RUN apt-get -y upgrade 6 | RUN apt-get -y install fakeroot apt-transport-https curl 7 | -------------------------------------------------------------------------------- /installers/deb/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | set -e 18 | # set -x 19 | SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )" 20 | . $SCRIPT_DIR/vars 21 | PROJECT_DIR="${SCRIPT_DIR}/../../" 22 | BINARY_FILE="${SCRIPT_DIR}/package/usr/bin/gocd-golang-agent" 23 | CONTROL_FILE="${SCRIPT_DIR}/package/DEBIAN/control" 24 | 25 | echo "############################" 26 | echo "Cross compiling for linux..." 27 | echo "############################" 28 | 29 | cd $PROJECT_DIR 30 | CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w' . 31 | mkdir -p `dirname $BINARY_FILE` 32 | cp gocd-golang-agent $BINARY_FILE 33 | chmod 0755 $BINARY_FILE 34 | 35 | echo "############################" 36 | echo "Packaging to deb file..." 37 | echo "############################" 38 | 39 | 40 | cd $SCRIPT_DIR 41 | 42 | if [ -f *.deb ] 43 | then 44 | rm *.deb 45 | fi 46 | 47 | eval $(docker-machine env) 48 | 49 | sed -i '' '/^Version:.*$/d' $CONTROL_FILE 50 | echo "Version: $GGA_VERSION-$GGA_DEB_VERSION" >> $CONTROL_FILE 51 | docker build -t gocd/deb-maker . 52 | docker run -v ${PWD}:/build gocd/deb-maker /bin/bash -c "cd /build/package && fakeroot dpkg-deb --build . ../$DEB_FILENAME" 53 | sed -i '' '/^Version:.*$/d' $CONTROL_FILE 54 | 55 | 56 | echo "############################" 57 | echo "Making sure installer can be installed..." 58 | echo "############################" 59 | 60 | docker run -v ${PWD}:/build gocd/deb-maker /bin/bash -c "cd /build && dpkg -i $DEB_FILENAME && service gocd-golang-agent start && sleep 3 && ps aux | grep -v 'grep' | grep gocd-golang-agent" 61 | 62 | echo "All check passed." 63 | -------------------------------------------------------------------------------- /installers/deb/package/DEBIAN/changelog: -------------------------------------------------------------------------------- 1 | Go (2.0) stable; urgency=low 2 | 3 | * This file is not updated, please refer to the ThoughtWorks Studios website 4 | 5 | -- Go Team Fri, 30 Jul 2010 15:00:00 +0000 6 | 7 | 8 | -------------------------------------------------------------------------------- /installers/deb/package/DEBIAN/conffiles: -------------------------------------------------------------------------------- 1 | /etc/default/gocd-golang-agent 2 | -------------------------------------------------------------------------------- /installers/deb/package/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: gocd-golang-agent 2 | Section: base 3 | Priority: optional 4 | Architecture: all 5 | Maintainer: The Go Team 6 | Conflicts: cruise-agent,go-agent 7 | Replaces: cruise-agent,go-agent 8 | Installed-Size: 5120 9 | Description: Go Agent Component 10 | next generation continuous integration and release management server from ThoughtWorks 11 | -------------------------------------------------------------------------------- /installers/deb/package/DEBIAN/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | set -e 19 | 20 | function go_owned { 21 | if [ -e "$1" ]; then 22 | chown -RL go:go "$1" 23 | if [ -L "$1" ]; then 24 | chown -h go:go "$1" 25 | fi 26 | fi 27 | } 28 | 29 | function create_if_does_not_exist { 30 | [ -d "$1" ] || mkdir "$1" 31 | go_owned "$1" 32 | } 33 | 34 | 35 | function set_go_agents_defaults_path { 36 | GOCD_AGENT_DEFAULTS=/etc/default/gocd-golang-agent 37 | } 38 | 39 | function fix_agent_defaults_ownership { 40 | go_owned $GOCD_AGENT_DEFAULTS || ( echo "user 'go' and group 'go' must exist" && exit 1 ) 41 | } 42 | 43 | function create_necessary_agent_directories { 44 | create_if_does_not_exist /var/log/gocd-golang-agent 45 | create_if_does_not_exist /var/run/gocd-golang-agent 46 | create_if_does_not_exist /var/lib/gocd-golang-agent 47 | } 48 | 49 | function print_agent_configuration_suggestions { 50 | echo "Now please edit $GOCD_AGENT_DEFAULTS and set GOCD_SERVER_URL to the HTTPS address of your Go Server. Please make sure the HOST in the URL is connectable from the agent machine." 51 | echo "Once that is done start the Go Agent with '/etc/init.d/gocd-golang-agent start'" 52 | } 53 | 54 | 55 | if [ "$1" = configure ]; then 56 | set_go_agents_defaults_path 57 | fix_agent_defaults_ownership 58 | create_necessary_agent_directories 59 | echo "Installation of GOCD Agent completed." 60 | print_agent_configuration_suggestions 61 | fi 62 | 63 | update-rc.d gocd-golang-agent defaults 99 >/dev/null 64 | -------------------------------------------------------------------------------- /installers/deb/package/DEBIAN/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | set -e 19 | 20 | if [ "$1" = "purge" ] ; then 21 | update-rc.d gocd-golang-agent remove >/dev/null || exit $? 22 | 23 | rm -fr /usr/share/gocd-golang-agent 24 | rm -fr /var/lib/gocd-golang-agent 25 | rm -fr /var/run/gocd-golang-agent 26 | rm -fr /var/log/gocd-golang-agent 27 | 28 | GO_AGENT_DEFAULTS=/etc/default/gocd-golang-agent 29 | if [ -L $GO_AGENT_DEFAULTS ]; then 30 | rm $GO_AGENT_DEFAULTS 31 | fi 32 | fi 33 | -------------------------------------------------------------------------------- /installers/deb/package/DEBIAN/preinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | set -e 19 | 20 | APP_NAME="'GOCD Agent'" 21 | 22 | function has_group { 23 | getent group $1 >/dev/null 24 | } 25 | 26 | function has_user { 27 | getent passwd $1 >/dev/null 28 | } 29 | 30 | function does_not_have_user { 31 | ! has_user $1 32 | } 33 | 34 | function does_not_have_group { 35 | ! has_group $1 36 | } 37 | 38 | function warn_about_go_existing_as { 39 | echo "$APP_NAME installation will now use $1 'go'" 40 | } 41 | 42 | function user_go_belongs_to_group_go { 43 | groups go | grep -q ':.*\' 44 | } 45 | 46 | function add_user_go_to_group_go { 47 | usermod -G go -a go 48 | } 49 | 50 | function warn_about_existing_user_or_group_go { 51 | (has_group go && warn_about_go_existing_as group) || true 52 | (has_user go && warn_about_go_existing_as user) || true 53 | } 54 | 55 | function create_user_go_if_none { 56 | (does_not_have_user go && create_user_go_with_group) || true 57 | } 58 | 59 | function create_group_go_if_none { 60 | (does_not_have_group go && create_group go) || true 61 | } 62 | 63 | function ensure_go_user_belongs_to_go_group { 64 | user_go_belongs_to_group_go || add_user_go_to_group_go 65 | } 66 | 67 | 68 | function create_group () { 69 | addgroup --system $1 70 | } 71 | 72 | function create_user_go_with_group () { 73 | adduser --system --group --gecos "Go User,,," --home /var/go --shell /bin/bash --firstuid 500 --lastuid 600 --disabled-password go 74 | } 75 | 76 | if [ -x "/etc/init.d/gocd-golang-agent" ]; then 77 | invoke-rc.d gocd-golang-agent stop || true 78 | fi 79 | 80 | 81 | if [ $1 = "install" ]; then 82 | warn_about_existing_user_or_group_go 83 | create_user_go_if_none 84 | create_group_go_if_none 85 | ensure_go_user_belongs_to_go_group 86 | fi 87 | -------------------------------------------------------------------------------- /installers/deb/package/DEBIAN/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | set -e 19 | 20 | if [ -x "/etc/init.d/gocd-golang-agent" ]; then 21 | invoke-rc.d gocd-golang-agent stop || true 22 | fi 23 | -------------------------------------------------------------------------------- /installers/deb/package/etc/default/gocd-golang-agent: -------------------------------------------------------------------------------- 1 | export GOCD_SERVER_URL=https://127.0.0.1:8154/go 2 | export GOCD_AGENT_WORKING_DIR=/var/lib/gocd-golang-agent 3 | export GOCD_AGENT_LOG_DIR=/var/log/gocd-golang-agent 4 | -------------------------------------------------------------------------------- /installers/deb/package/etc/init.d/gocd-golang-agent: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | ### BEGIN INIT INFO 19 | # Provides: gocd-golang-agent 20 | # Required-Start: $network $remote_fs 21 | # Required-Stop: $network $remote_fs 22 | # Default-Start: 2 3 4 5 23 | # Default-Stop: 0 1 6 24 | # Description: Start the GOCD Agent 25 | ### END INIT INFO 26 | NAME="gocd-golang-agent" 27 | PIDFILE="/var/run/gocd-golang-agent/gocd-golang-agent.pid" 28 | DAEMON="/usr/bin/gocd-golang-agent" 29 | 30 | . /etc/default/gocd-golang-agent 31 | 32 | case "$1" in 33 | start) 34 | echo -n "Starting daemon: "$NAME 35 | /sbin/start-stop-daemon --start -c go:go -m --quiet --pidfile $PIDFILE --background --exec $DAEMON 36 | echo "." 37 | ;; 38 | stop) 39 | echo -n "Stopping daemon: "$NAME 40 | /sbin/start-stop-daemon --stop -c go:go --quiet --oknodo --pidfile $PIDFILE 41 | echo "." 42 | ;; 43 | restart) 44 | echo -n "Restarting daemon: "$NAME 45 | /sbin/start-stop-daemon --stop -c go:go --quiet --oknodo --retry 30 --pidfile $PIDFILE 46 | /sbin/start-stop-daemon --start -c go:go -m --quiet --pidfile $PIDFILE --background --exec $DAEMON -- $DAEMON_OPTS 47 | echo "." 48 | ;; 49 | 50 | *) 51 | echo "Usage: "$1" {start|stop|restart}" 52 | exit 1 53 | esac 54 | exit 0 55 | -------------------------------------------------------------------------------- /installers/deb/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | set -e 18 | SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )" 19 | . $SCRIPT_DIR/vars 20 | $SCRIPT_DIR/build.sh 21 | 22 | echo "############################" 23 | echo "Publishing package to bintray repos" 24 | echo "############################" 25 | 26 | curl -T "$DEB_FILENAME" -ualex-hal9000:${GGA_RELEASE_KEY:?"need set GGA_RELEASE_KEY"} "https://api.bintray.com/content/alex-hal9000/gocd-golang-agent/gocd-golang-agent/${GGA_VERSION}/${DEB_FILENAME};deb_distribution=master;deb_component=main;deb_architecture=amd64;publish=1" 27 | 28 | echo 29 | echo "############################" 30 | echo "testing new version of package is accessible" 31 | echo "############################" 32 | echo "Wait 30 seconds to package info get updated in bintray service. You can ctrl-c now if you do not want verify the published package" 33 | sleep 30 34 | docker run gocd/deb-maker /bin/bash -c "echo 'deb https://dl.bintray.com/alex-hal9000/gocd-golang-agent master main' > /etc/apt/sources.list && apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 379CE192D401AB61 && apt-get update && apt-get -y --force-yes install gocd-golang-agent && dpkg -s gocd-golang-agent" 35 | -------------------------------------------------------------------------------- /installers/deb/vars: -------------------------------------------------------------------------------- 1 | export GGA_VERSION=0.1 2 | export GGA_DEB_VERSION=18 3 | export DEB_FILENAME="gocd-golang-agent_${GGA_VERSION}-${GGA_DEB_VERSION}_amd64.deb" 4 | -------------------------------------------------------------------------------- /installers/rpm/agent.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #*************************GO-LICENSE-START******************************** 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | #*************************GO-LICENSE-END********************************** 17 | 18 | 19 | SERVICE_NAME=${1:-gocd-golang-agent} 20 | 21 | if [ -f /etc/default/${SERVICE_NAME} ]; then 22 | echo "[`date`] using default settings from /etc/default/${SERVICE_NAME}" 23 | . /etc/default/${SERVICE_NAME} 24 | fi 25 | 26 | CWD=`dirname "$0"` 27 | 28 | GOCD_SERVER_URL=${GOCD_SERVER_URL:-"https://127.0.0.1:8154/go"} 29 | VNC=${VNC:-"N"} 30 | GOCD_AGENT_WORK_DIR=${GOCD_AGENT_WORK_DIR:-"/var/lib/gocd-golang-agent"} 31 | 32 | if [ ! -d "${GOCD_AGENT_WORK_DIR}" ]; then 33 | echo Agent working directory ${GOCD_AGENT_WORK_DIR} does not exist 34 | exit 2 35 | fi 36 | 37 | GOCD_AGENT_LOG_DIR=/var/log/${SERVICE_NAME} 38 | 39 | STDOUT_LOG_FILE=$GOCD_AGENT_LOG_DIR/${SERVICE_NAME}.log 40 | 41 | PID_FILE="$GOCD_AGENT_WORK_DIR/gocd-golang-agent.pid" 42 | 43 | 44 | if [ "$VNC" == "Y" ]; then 45 | echo "[`date`] Starting up VNC on :3" 46 | /usr/bin/vncserver :3 47 | DISPLAY=:3 48 | export DISPLAY 49 | fi 50 | 51 | export GOCD_AGENT_LOG_DIR 52 | export LOG_FILE 53 | 54 | CMD="/opt/${SERVICE_NAME}/bin/${SERVICE_NAME}" 55 | 56 | cd "$GOCD_AGENT_WORK_DIR" 57 | 58 | if [ "$DAEMON" == "Y" ]; then 59 | eval exec nohup "$CMD" >>"$STDOUT_LOG_FILE" 2>&1 & 60 | echo $! >"$PID_FILE" 61 | else 62 | eval exec "$CMD" 63 | fi 64 | 65 | -------------------------------------------------------------------------------- /installers/rpm/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | set -e 18 | # set -x 19 | SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )" 20 | . $SCRIPT_DIR/vars 21 | PROJECT_DIR="${SCRIPT_DIR}/../../" 22 | RPM_BUILD_DIR="${PROJECT_DIR}/tmp_build" 23 | export GOPATH=$PROJECT_DIR 24 | 25 | go get golang.org/x/net/websocket 26 | go get github.com/satori/go.uuid 27 | go get github.com/xli/assert 28 | go get github.com/bmatcuk/doublestar 29 | go get github.com/jstemmer/go-junit-report 30 | mkdir -p $PROJECT_DIR/src/github.com/gocd-contrib/gocd-golang-agent/ 31 | if [ ! -d $PROJECT_DIR/src/github.com/gocd-contrib/gocd-golang-agent/agent ]; then 32 | ln -s $PROJECT_DIR/agent $PROJECT_DIR/src/github.com/gocd-contrib/gocd-golang-agent/agent 33 | fi 34 | if [ ! -d $PROJECT_DIR/src/github.com/gocd-contrib/gocd-golang-agent/junit ]; then 35 | ln -s $PROJECT_DIR/junit $PROJECT_DIR/src/github.com/gocd-contrib/gocd-golang-agent/junit 36 | fi 37 | if [ ! -d $PROJECT_DIR/src/github.com/gocd-contrib/gocd-golang-agent/protocol ]; then 38 | ln -s $PROJECT_DIR/protocol $PROJECT_DIR/src/github.com/gocd-contrib/gocd-golang-agent/protocol 39 | fi 40 | if [ ! -d $PROJECT_DIR/src/github.com/gocd-contrib/gocd-golang-agent/stream ]; then 41 | ln -s $PROJECT_DIR/stream $PROJECT_DIR/src/github.com/gocd-contrib/gocd-golang-agent/stream 42 | fi 43 | 44 | echo "################################" 45 | echo "Create Temporary Build Structure" 46 | echo "################################" 47 | mkdir -p $RPM_BUILD_DIR/gocd-golang-agent-$GGA_VERSION/opt/gocd-golang-agent/bin 48 | mkdir -p $RPM_BUILD_DIR/gocd-golang-agent-$GGA_VERSION/etc/init.d 49 | mkdir -p $RPM_BUILD_DIR/gocd-golang-agent-$GGA_VERSION/etc/default 50 | mkdir -p $RPM_BUILD_DIR/BUILD 51 | mkdir -p $RPM_BUILD_DIR/BUILDROOT 52 | mkdir -p $RPM_BUILD_DIR/RPMS 53 | mkdir -p $RPM_BUILD_DIR/SOURCES 54 | mkdir -p $RPM_BUILD_DIR/SPECS 55 | mkdir -p $RPM_BUILD_DIR/SRPMS 56 | mkdir -p $PROJECT_DIR/build_installers 57 | 58 | echo "############################" 59 | echo "Cross compiling for linux..." 60 | echo "############################" 61 | 62 | cd $PROJECT_DIR 63 | CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w' . 64 | chmod 0755 gocd-golang-agent 65 | cp gocd-golang-agent $RPM_BUILD_DIR/gocd-golang-agent-$GGA_VERSION/opt/gocd-golang-agent/bin 66 | 67 | echo "############################" 68 | echo "Packaging rpm source file..." 69 | echo "############################" 70 | cd $SCRIPT_DIR 71 | install -m 755 agent.sh $RPM_BUILD_DIR/gocd-golang-agent-$GGA_VERSION/opt/gocd-golang-agent/bin 72 | install -m 755 gocd-golang-agent_default $RPM_BUILD_DIR/gocd-golang-agent-$GGA_VERSION/etc/default/gocd-golang-agent 73 | install -m 755 gocd-golang-agent_init.d $RPM_BUILD_DIR/gocd-golang-agent-$GGA_VERSION/etc/init.d/gocd-golang-agent 74 | cat << EOF > $RPM_BUILD_DIR/gocd-golang-agent-$GGA_VERSION/etc/default/gocd-golang-agent 75 | export GOCD_SERVER_URL=https://localhost:8154/go 76 | export GOCD_AGENT_WORKING_DIR=/var/lib/gocd-golang-agent 77 | export GOCD_AGENT_LOG_DIR=/var/log/gocd-golang-agent 78 | EOF 79 | cd $RPM_BUILD_DIR 80 | tar zcvf gocd-golang-agent-$GGA_VERSION.tar.gz gocd-golang-agent-$GGA_VERSION 81 | mv gocd-golang-agent-$GGA_VERSION.tar.gz $RPM_BUILD_DIR/SOURCES 82 | cp $SCRIPT_DIR/gocd-golang-agent.spec $RPM_BUILD_DIR/SPECS 83 | rpmbuild -bb --define '_topdir '$RPM_BUILD_DIR $RPM_BUILD_DIR/SPECS/gocd-golang-agent.spec 84 | mv $RPM_BUILD_DIR/RPMS/`uname -m`/gocd-golang-agent-$GGA_VERSION-$GGA_RPM_VERSION.`uname -m`.rpm $PROJECT_DIR/build_installers 85 | rm -rf $RPM_BUILD_DIR 86 | 87 | 88 | -------------------------------------------------------------------------------- /installers/rpm/gocd-golang-agent.spec: -------------------------------------------------------------------------------- 1 | Name: gocd-golang-agent 2 | Version: 0.1 3 | Release: 1 4 | Summary: GoCD Golang Agent 5 | License: Apache License Version 2.0 6 | URL: https://github.com/gocd-contrib/gocd-golang-agent 7 | SOURCE: gocd-golang-agent-0.1.tar.gz 8 | 9 | %description 10 | GoCD Golang Agent 11 | 12 | %prep 13 | %setup 14 | 15 | %install 16 | rm -rf "$RPM_BUILD_ROOT" 17 | mkdir -p "$RPM_BUILD_ROOT/opt/%{name}/bin" 18 | mkdir -p "$RPM_BUILD_ROOT/etc/init.d" 19 | mkdir -p "$RPM_BUILD_ROOT/etc/default" 20 | mkdir -p "$RPM_BUILD_ROOT/var/log/%{name}" 21 | mkdir -p "$RPM_BUILD_ROOT/var/lib/%{name}" 22 | cp opt/%{name}/bin/agent.sh "$RPM_BUILD_ROOT/opt/%{name}/bin" 23 | cp opt/%{name}/bin/gocd-golang-agent "$RPM_BUILD_ROOT/opt/%{name}/bin" 24 | cp etc/init.d/gocd-golang-agent "$RPM_BUILD_ROOT/etc/init.d" 25 | cp etc/default/gocd-golang-agent "$RPM_BUILD_ROOT/etc/default" 26 | 27 | %pre 28 | getent group go >/dev/null || groupadd go 29 | getent passwd go >/dev/null || \ 30 | useradd -g go -d /var/go -s /bin/bash \ 31 | -c "GoCD Golang Agent" go 32 | mkdir -p /var/lib/%{name} 33 | mkdir -p /var/log/%{name} 34 | 35 | %preun 36 | service %{name} stop 37 | 38 | %postun 39 | rm -rf /var/log/%{name} 40 | rm -rf /var/lib/%{name} 41 | 42 | %files 43 | /opt/%{name} 44 | /etc/init.d/gocd-golang-agent 45 | /etc/default/gocd-golang-agent 46 | 47 | %attr(755, root, root) /etc/init.d/gocd-golang-agent 48 | %attr(644, go, go) /etc/default/gocd-golang-agent 49 | %attr(755, go, go) /opt/%{name}/bin/gocd-golang-agent 50 | %attr(755, go, go) /opt/%{name}/bin/agent.sh 51 | %attr(755, go, go) /var/log/%{name} 52 | %attr(755, go, go) /var/lib/%{name} 53 | 54 | %clean 55 | rm -rf "$RPM_BUILD_ROOT" 56 | 57 | %changelog 58 | * Fri May 27 2016 Barrow Kwan 1.0-1 59 | - First Build 60 | -------------------------------------------------------------------------------- /installers/rpm/gocd-golang-agent_default: -------------------------------------------------------------------------------- 1 | export GOCD_SERVER_URL=https://localhost:8154/go 2 | export GOCD_AGENT_WORKING_DIR=/var/lib/gocd-golang-agent 3 | export GOCD_AGENT_LOG_DIR=/var/log/gocd-golang-agent -------------------------------------------------------------------------------- /installers/rpm/gocd-golang-agent_init.d: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #*************************GO-LICENSE-START******************************** 3 | # Copyright 2014 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | #*************************GO-LICENSE-END********************************** 17 | 18 | # chkconfig: 2345 90 90 19 | # description: GoCD Golang Agent 20 | # processname: java 21 | ### BEGIN INIT INFO 22 | # Provides: gocd-golang-agent 23 | # Required-Start: $network $remote_fs 24 | # Required-Stop: $network $remote_fs 25 | # Default-Start: 2 3 4 5 26 | # Default-Stop: 0 1 6 27 | # Description: Start the GoCD Golang Agent 28 | ### END INIT INFO 29 | SERVICE_NAME=${0##*/} 30 | 31 | # Strips initV leading chars if the script is located in rcX.d (automatically executed on runlevel change) 32 | INIT_DIR=${0%/*}; echo ${INIT_DIR##*/} | grep -e '^rc[0-9]\.d$' >/dev/null && SERVICE_NAME=$(echo "${SERVICE_NAME}" | sed 's/^[SK][0-9][0-9]//'); 33 | 34 | PID_FILE="/var/run/${SERVICE_NAME}/${SERVICE_NAME}.pid" 35 | CUR_USER=`whoami` 36 | 37 | # LSB implimentation is not standard across distros, so we have to roll our own... 38 | killproc() { 39 | pkill -u go -f /opt/${SERVICE_NAME}/bin/${SERVICE_NAME} 40 | } 41 | 42 | start_daemon() { 43 | eval "$@" 44 | } 45 | 46 | log_success_msg() { 47 | echo "$@" 48 | } 49 | 50 | log_failure_msg() { 51 | echo "$@" 52 | } 53 | 54 | . /etc/default/${SERVICE_NAME} 55 | 56 | check_proc() { 57 | pgrep -u go -f /opt/${SERVICE_NAME}/bin/${SERVICE_NAME} >/dev/null 58 | } 59 | 60 | start_go_agent() { 61 | if [ "${CUR_USER}" != "root" ] && [ "${CUR_USER}" != "go" ]; then 62 | log_failure_msg "Go Agent can only be started as 'root' or 'go' user." 63 | exit 4 64 | fi 65 | 66 | check_proc 67 | if [ $? -eq 0 ]; then 68 | log_success_msg "Go Agent already running." 69 | exit 0 70 | fi 71 | 72 | [ -d /var/run/${SERVICE_NAME} ] || (mkdir /var/run/${SERVICE_NAME} && chown -R go:go /var/run/${SERVICE_NAME}) 73 | 74 | if [ "${CUR_USER}" == "root" ]; then 75 | start_daemon "/bin/su - go -c '/opt/${SERVICE_NAME}/bin/agent.sh'" 76 | else 77 | start_daemon /opt/${SERVICE_NAME}/bin/agent.sh ${SERVICE_NAME} 78 | fi 79 | 80 | # Sleep for a while to see if Cruise bleats about anything 81 | sleep 15 82 | check_proc 83 | 84 | if [ $? -eq 0 ]; then 85 | log_success_msg "Started GoCD Golang Agent." 86 | else 87 | log_failure_msg "Error starting GoCD Golang Agent." 88 | exit -1 89 | fi 90 | } 91 | 92 | stop_go_agent() { 93 | if [ "${CUR_USER}" != "root" ] && [ "${CUR_USER}" != "go" ]; then 94 | log_failure_msg "You do not have permission to stop the GoCD Golang Agent" 95 | exit 4 96 | fi 97 | 98 | check_proc 99 | 100 | if [ $? -eq 0 ]; then 101 | killproc -p $PID_FILE /opt/${SERVICE_NAME}/bin/agent.sh >/dev/null 102 | 103 | # Make sure it's dead before we return 104 | until [ $? -ne 0 ]; do 105 | sleep 1 106 | check_proc 107 | done 108 | 109 | check_proc 110 | if [ $? -eq 0 ]; then 111 | log_failure_msg "Error stopping Go Agent." 112 | exit -1 113 | else 114 | log_success_msg "Stopped GoCD Golang Agent." 115 | fi 116 | else 117 | log_failure_msg "Go is not running or you don't have permission to stop it" 118 | fi 119 | } 120 | 121 | go_status() { 122 | check_proc 123 | if [ $? -eq 0 ]; then 124 | log_success_msg "GoCD Golang Agent is running." 125 | else 126 | log_failure_msg "GoCD Golang Agent is stopped." 127 | exit 3 128 | fi 129 | } 130 | 131 | case "$1" in 132 | start) 133 | start_go_agent 134 | ;; 135 | stop) 136 | stop_go_agent 137 | ;; 138 | restart) 139 | stop_go_agent 140 | start_go_agent 141 | ;; 142 | status) 143 | go_status 144 | ;; 145 | *) 146 | echo "Usage: $0 {start|stop|restart|status}" 147 | exit 1 148 | esac 149 | 150 | exit 0 151 | -------------------------------------------------------------------------------- /installers/rpm/vars: -------------------------------------------------------------------------------- 1 | export GGA_VERSION=0.1 2 | export GGA_RPM_VERSION=1 3 | -------------------------------------------------------------------------------- /junit/test/junit_illegal_report.xml: -------------------------------------------------------------------------------- 1 | 2 | illeag xml file 3 | -------------------------------------------------------------------------------- /junit/test/junit_report1.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /junit/test/junit_report2.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /junit/test/junit_report3.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /junit/xml_report.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package junit 18 | 19 | import ( 20 | "encoding/xml" 21 | "io/ioutil" 22 | "os" 23 | ) 24 | 25 | type TestSuites struct { 26 | XMLName xml.Name `xml:"testsuites"` 27 | 28 | TestSuites []*TestSuite `xml:"testsuite"` 29 | } 30 | 31 | type TestSuite struct { 32 | XMLName xml.Name `xml:"testsuite"` 33 | 34 | Failures int `xml:"failures,attr"` 35 | Time float64 `xml:"time,attr"` 36 | Errors int `xml:"errors,attr"` 37 | Skipped int `xml:"skipped,attr"` 38 | Tests int `xml:"tests,attr"` 39 | Name string `xml:"name,attr"` 40 | 41 | TestCases []*TestCase `xml:"testcase"` 42 | } 43 | 44 | func (s *TestSuite) Merge(another *TestSuite) { 45 | s.Failures += another.Failures 46 | s.Time += another.Time 47 | s.Errors += another.Errors 48 | s.Skipped += another.Skipped 49 | s.Tests += another.Tests 50 | s.TestCases = append(s.TestCases, another.TestCases...) 51 | } 52 | 53 | type TestCase struct { 54 | XMLName xml.Name `xml:"testcase"` 55 | 56 | Time float64 `xml:"time,attr"` 57 | ClassName string `xml:"classname,attr"` 58 | Name string `xml:"name,attr"` 59 | 60 | Skipped *Skipped `xml:"skipped"` 61 | Error *Error `xml:"error"` 62 | Failure *Failure `xml:"failure"` 63 | SystemOut *SystemOut `xml:"system-out"` 64 | } 65 | 66 | type Skipped struct { 67 | XMLName xml.Name `xml:"skipped"` 68 | } 69 | 70 | type Error struct { 71 | XMLName xml.Name `xml:"error"` 72 | 73 | Type string `xml:"type,attr"` 74 | StackTrace string `xml:",chardata"` 75 | } 76 | 77 | type Failure struct { 78 | XMLName xml.Name `xml:"failure"` 79 | 80 | Message string `xml:"message,attr"` 81 | Type string `xml:"type,attr"` 82 | StackTrace string `xml:",chardata"` 83 | } 84 | 85 | type SystemOut struct { 86 | XMLName xml.Name `xml:"system-out"` 87 | 88 | Log string `xml:",chardata"` 89 | } 90 | 91 | func (t *TestCase) Type() string { 92 | if t.IsFail() { 93 | return "Failure" 94 | } 95 | if t.IsError() { 96 | return "Error" 97 | } 98 | return "" 99 | } 100 | 101 | func (t *TestCase) IsFail() bool { 102 | return t.Failure != nil 103 | } 104 | 105 | func (t *TestCase) IsError() bool { 106 | return t.Error != nil 107 | } 108 | 109 | func (t *TestCase) IsSkip() bool { 110 | return t.Skipped != nil 111 | } 112 | 113 | func NewTestSuite() *TestSuite { 114 | return &TestSuite{ 115 | TestCases: make([]*TestCase, 0), 116 | } 117 | } 118 | 119 | func Read(f string) (suite *TestSuite, err error) { 120 | data, err := ioutil.ReadFile(f) 121 | if err != nil { 122 | return 123 | } 124 | 125 | suite = NewTestSuite() 126 | err = xml.Unmarshal(data, suite) 127 | if err != nil { 128 | if isTestSuites(err) { 129 | suites := &TestSuites{ 130 | TestSuites: make([]*TestSuite, 0), 131 | } 132 | err = xml.Unmarshal(data, suites) 133 | if err != nil { 134 | return 135 | } 136 | for _, s := range suites.TestSuites { 137 | suite.Merge(s) 138 | } 139 | } else { 140 | return 141 | } 142 | } 143 | return 144 | } 145 | 146 | func GenerateJunitTestReport(result *TestSuite, path string) (err error){ 147 | info, err := os.Stat(path) 148 | if err != nil { 149 | return 150 | } 151 | if info.IsDir() { 152 | return 153 | } 154 | suite, err := Read(path) 155 | if err != nil { 156 | return 157 | } 158 | result.Merge(suite) 159 | return 160 | } 161 | 162 | func isTestSuites(err error) bool { 163 | return err.Error() == "expected element type but have " 164 | } 165 | -------------------------------------------------------------------------------- /junit/xml_report_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package junit_test 17 | 18 | import ( 19 | . "github.com/gocd-contrib/gocd-golang-agent/junit" 20 | "github.com/xli/assert" 21 | "path/filepath" 22 | "runtime" 23 | "testing" 24 | ) 25 | 26 | func TestReadReport(t *testing.T) { 27 | suite, err := Read(filepath.Join(DIR(), "test", "junit_report1.xml")) 28 | assert.Nil(t, err) 29 | assert.Equal(t, 1, suite.Tests) 30 | } 31 | 32 | func TestReadIllegalReport(t *testing.T) { 33 | _, err := Read(filepath.Join(DIR(), "test", "junit_illegal_report.xml")) 34 | assert.NotNil(t, err) 35 | } 36 | 37 | func TestReadTestSuitesReport(t *testing.T) { 38 | suite, err := Read(filepath.Join(DIR(), "test", "junit_report3.xml")) 39 | assert.Nil(t, err) 40 | assert.Equal(t, 1, suite.Tests) 41 | } 42 | 43 | func TestMergeSuite(t *testing.T) { 44 | suite := NewTestSuite() 45 | suite1, err := Read(filepath.Join(DIR(), "test", "junit_report1.xml")) 46 | assert.Nil(t, err) 47 | suite.Merge(suite1) 48 | 49 | suite2, err := Read(filepath.Join(DIR(), "test", "junit_report2.xml")) 50 | assert.Nil(t, err) 51 | suite.Merge(suite2) 52 | 53 | suite3, err := Read(filepath.Join(DIR(), "test", "junit_report3.xml")) 54 | assert.Nil(t, err) 55 | suite.Merge(suite3) 56 | 57 | assert.Equal(t, 4, suite.Tests) 58 | assert.Equal(t, 1, suite.Failures) 59 | assert.Equal(t, 4, len(suite.TestCases)) 60 | } 61 | 62 | func DIR() string { 63 | _, filename, _, _ := runtime.Caller(1) 64 | return filepath.Dir(filename) 65 | } 66 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "github.com/gocd-contrib/gocd-golang-agent/agent" 21 | "time" 22 | "flag" 23 | "fmt" 24 | "os" 25 | ) 26 | 27 | var ( 28 | Version = "0.0" 29 | Githash = "No Version Provided" 30 | ) 31 | 32 | func main() { 33 | 34 | versonPtr := flag.Bool("version", false, "Show GoCD Golang Agent Verson") 35 | flag.Parse() 36 | 37 | if *versonPtr { 38 | fmt.Println("GoCD Golang Agent Verson : ", Version, " (", Githash, ")") 39 | os.Exit(0) 40 | } 41 | 42 | agent.Initialize() 43 | for { 44 | err := agent.Start() 45 | if err != nil { 46 | agent.LogInfo("something wrong: %v", err.Error()) 47 | } 48 | agent.LogInfo("sleep 10 seconds and restart") 49 | time.Sleep(10 * time.Second) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /nunit/test/illegal_report.xml: -------------------------------------------------------------------------------- 1 | 2 | illeag xml file 3 | -------------------------------------------------------------------------------- /nunit/xml_report.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package nunit 18 | 19 | import ( 20 | "encoding/xml" 21 | "io/ioutil" 22 | "os" 23 | ) 24 | 25 | type TestResults struct { 26 | XMLName xml.Name `xml:"test-results"` 27 | 28 | Name string `xml:"name,attr"` 29 | Total int `xml:"total,attr"` 30 | Errors int `xml:"errors,attr"` 31 | Failures int `xml:"failures,attr"` 32 | NotRun int `xml:"not-run,attr"` 33 | Ignored int `xml:"ignored,attr"` 34 | Skipped int `xml:"skipped,attr"` 35 | Invalid int `xml:"invalid,attr"` 36 | 37 | Environment *Environment `xml:"environment"` 38 | TestSuite *TestSuite `xml:"test-suite"` 39 | 40 | Time float64 41 | TestCases []*TestCase 42 | } 43 | 44 | type Environment struct { 45 | XMLName xml.Name `xl:"environment"` 46 | NUnitVersion string `xml:"nunit-version,attr"` 47 | ClrVersion string `xml:"clr-version,attr"` 48 | OsVersion string `xml:"os-version,attr"` 49 | Platform string `xml:"platform,attr"` 50 | Cwd string `xml:"cwd,attr"` 51 | MachineName string`xml:"machine-name,attr"` 52 | User string `xml:"user,attr"` 53 | UserDomain string `xml:"user-domain,attr"` 54 | } 55 | 56 | type TestSuite struct { 57 | XMLName xml.Name `xml:"test-suite"` 58 | 59 | Name string `xml:"name,attr"` 60 | Executed bool `xml:"executed,attr"` 61 | Success bool `xml:"success,attr"` 62 | Time float64 `xml:"time,attr"` 63 | Asserts int `xml:"asserts,attr"` 64 | 65 | Categories *Categories `xml:"categories"` 66 | Properties *Properties `xml:"properties"` 67 | Failure *Failure `xml:"failure"` 68 | Reason *Reason `xml:"reason"` 69 | 70 | TestSuites []*TestSuite `xml:"results>test-suite"` 71 | TestCases []*TestCase `xml:"results>test-case"` 72 | } 73 | 74 | func (t *TestSuite) InternalTestCases() []*TestCase { 75 | testCases := t.TestCases 76 | if testCases != nil { 77 | return testCases 78 | } else { 79 | for _, suite := range t.TestSuites { 80 | testCases = append(testCases, suite.InternalTestCases()...) 81 | } 82 | return testCases 83 | } 84 | } 85 | 86 | type TestCase struct { 87 | XMLName xml.Name `xml:"test-case"` 88 | 89 | Name string `xml:"name,attr"` 90 | Description string `xml:"description,attr"` 91 | Success string `xml:"success,attr"` 92 | Time float64 `xml:"time.attr"` 93 | Executed bool `xml:"executed,attr"` 94 | Asserts int `xml:"asserts,attr"` 95 | 96 | Categories *Categories `xml:"categories"` 97 | Properties *Properties `xml:"properties"` 98 | Failure *Failure `xml:"failure"` 99 | Reason *Reason `xml:"reason"` 100 | } 101 | 102 | type Categories struct { 103 | Categories []*Category `xml:"category"` 104 | } 105 | 106 | type Category struct { 107 | XMLName xml.Name `xml:"category"` 108 | Name string `xml:"name,attr"` 109 | } 110 | 111 | type Properties struct { 112 | XMLName xml.Name `xml:"properties"` 113 | Properties []*Property `xml:"property"` 114 | } 115 | 116 | type Property struct { 117 | XMLName xml.Name `xml:"property"` 118 | Name string `xml:"name,attr"` 119 | Value string `xml:"value,attr"` 120 | } 121 | 122 | type Failure struct { 123 | XMLName xml.Name `xml:"failure"` 124 | Message *Message `xml:"message"` 125 | StackTrace *StackTrace `xml:"stack-trace"` 126 | } 127 | 128 | type Reason struct { 129 | XMLName xml.Name `xml:"reason"` 130 | Message *Message `xml:"message"` 131 | } 132 | 133 | type Message struct { 134 | XMLName xml.Name `xml:"message"` 135 | Content string `xml:",chardata"` 136 | } 137 | 138 | type StackTrace struct { 139 | XMLName xml.Name `xml:"stack-trace"` 140 | Content string `xml:",chardata"` 141 | } 142 | 143 | func NewTestResults() *TestResults { 144 | return new(TestResults) 145 | } 146 | 147 | func (t *TestResults) Merge(another *TestResults) { 148 | t.Total += another.Total 149 | t.Errors += another.Errors 150 | t.Failures += another.Failures 151 | t.NotRun += another.NotRun 152 | t.Ignored += another.Ignored 153 | t.Skipped += another.Skipped 154 | t.Invalid += another.Invalid 155 | t.Time += another.Time 156 | t.TestCases = append(t.TestCases, another.TestCases...) 157 | } 158 | 159 | func Read(f string) (results *TestResults, err error) { 160 | data, err := ioutil.ReadFile(f) 161 | if err != nil { 162 | return 163 | } 164 | results = NewTestResults() 165 | err = xml.Unmarshal(data, results) 166 | 167 | if err !=nil { 168 | return 169 | } 170 | 171 | results.Time = results.TestSuite.Time 172 | results.TestCases = results.TestSuite.InternalTestCases() 173 | 174 | return 175 | } 176 | 177 | func GenerateNUnitTestReport(result *TestResults, path string) (err error){ 178 | info, err := os.Stat(path) 179 | if err != nil { 180 | return 181 | } 182 | if info.IsDir() { 183 | return 184 | } 185 | suite, err := Read(path) 186 | if err != nil { 187 | return 188 | } 189 | result.Merge(suite) 190 | return 191 | } 192 | 193 | 194 | -------------------------------------------------------------------------------- /nunit/xml_report_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package nunit_test 18 | 19 | import ( 20 | . "github.com/gocd-contrib/gocd-golang-agent/nunit" 21 | "testing" 22 | "path/filepath" 23 | "runtime" 24 | "github.com/xli/assert" 25 | ) 26 | 27 | func TestReadReportForNUnit2XReport(t *testing.T) { 28 | results, err := Read(filepath.Join(DIR(), "test", "nunit2x_report1.xml")) 29 | 30 | assert.Nil(t, err) 31 | assert.Equal(t, 8, results.Total) 32 | assert.Equal(t, 1, results.Errors) 33 | assert.Equal(t, 1, results.Failures) 34 | assert.Equal(t, 7, results.NotRun) 35 | assert.Equal(t, 4, results.Ignored) 36 | assert.Equal(t, 0, results.Skipped) 37 | assert.Equal(t, 15, len(results.TestCases)) 38 | assert.Equal(t, 0.125, results.Time) 39 | } 40 | 41 | func TestReadReportForIllegalXml(t *testing.T) { 42 | _, err := Read(filepath.Join(DIR(), "test", "illegal_report.xml")) 43 | assert.NotNil(t, err) 44 | } 45 | 46 | func TestReadReportMergeNUnit2XReport(t *testing.T) { 47 | 48 | result := NewTestResults() 49 | 50 | results1, err1 := Read(filepath.Join(DIR(), "test", "nunit2x_report1.xml")) 51 | assert.Nil(t, err1) 52 | result.Merge(results1) 53 | 54 | results2, err2 := Read(filepath.Join(DIR(), "test", "nunit2x_report2.xml")) 55 | assert.Nil(t, err2) 56 | result.Merge(results2) 57 | 58 | assert.Equal(t, 16, result.Total) 59 | assert.Equal(t, 2, result.Errors) 60 | assert.Equal(t, 2, result.Failures) 61 | assert.Equal(t, 14, result.NotRun) 62 | assert.Equal(t, 8, result.Ignored) 63 | assert.Equal(t, 0, result.Skipped) 64 | assert.Equal(t, 0.25, result.Time) 65 | assert.Equal(t, 30, len(result.TestCases)) 66 | } 67 | 68 | func DIR() string { 69 | _, filename, _, _ := runtime.Caller(1) 70 | return filepath.Dir(filename) 71 | } -------------------------------------------------------------------------------- /protocol/agent_runtime_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package protocol 18 | 19 | type AgentIdentifier struct { 20 | HostName string `json:"hostName"` 21 | IpAddress string `json:"ipAddress"` 22 | Uuid string `json:"uuid"` 23 | } 24 | 25 | type AgentBuildingInfo struct { 26 | BuildingInfo string `json:"buildingInfo"` 27 | BuildLocator string `json:"buildLocator"` 28 | } 29 | 30 | type AgentRuntimeInfo struct { 31 | Identifier *AgentIdentifier `json:"identifier"` 32 | BuildingInfo *AgentBuildingInfo `json:"buildingInfo"` 33 | RuntimeStatus string `json:"runtimeStatus"` 34 | Location string `json:"location"` 35 | UsableSpace int64 `json:"usableSpace"` 36 | OperatingSystemName string `json:"operatingSystemName"` 37 | Cookie string `json:"cookie"` 38 | AgentLauncherVersion string `json:"agentLauncherVersion"` 39 | ElasticPluginId string `json:"elasticPluginId"` 40 | ElasticAgentId string `json:"elasticAgentId"` 41 | SupportsBuildCommandProtocol bool `json:"supportsBuildCommandProtocol"` 42 | } 43 | -------------------------------------------------------------------------------- /protocol/build.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package protocol 18 | 19 | const ( 20 | BuildPassed = "Passed" 21 | BuildFailed = "Failed" 22 | BuildCanceled = "Cancelled" 23 | ) 24 | 25 | type Build struct { 26 | BuildId string 27 | BuildLocator string 28 | BuildLocatorForDisplay string 29 | ConsoleUrl string 30 | ArtifactUploadBaseUrl string 31 | PropertyBaseUrl string 32 | BuildCommand *BuildCommand 33 | } 34 | -------------------------------------------------------------------------------- /protocol/build_command.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package protocol 18 | 19 | import ( 20 | "encoding/json" 21 | "strings" 22 | ) 23 | 24 | const ( 25 | TestReportFileName = "index.html" 26 | 27 | RunIfConfigAny = "any" 28 | RunIfConfigPassed = "passed" 29 | ExecInput = "" 30 | 31 | CommandCompose = "compose" 32 | CommandCond = "cond" 33 | CommandAnd = "and" 34 | CommandOr = "or" 35 | CommandExport = "export" 36 | CommandTest = "test" 37 | CommandExec = "exec" 38 | CommandEcho = "echo" 39 | CommandUploadArtifact = "uploadArtifact" 40 | CommandReportCurrentStatus = "reportCurrentStatus" 41 | CommandReportCompleting = "reportCompleting" 42 | CommandMkdirs = "mkdirs" 43 | CommandCleandir = "cleandir" 44 | CommandFail = "fail" 45 | CommandSecret = "secret" 46 | CommandDownloadFile = "downloadFile" 47 | CommandDownloadDir = "downloadDir" 48 | CommandGenerateTestReport = "generateTestReport" 49 | CommandGenerateProperty = "generateProperty" 50 | ) 51 | 52 | type BuildCommand struct { 53 | Name string 54 | Args map[string]string 55 | RunIfConfig string 56 | ExecInput string 57 | SubCommands []*BuildCommand 58 | WorkingDirectory string 59 | Test *BuildCommand 60 | OnCancel *BuildCommand 61 | } 62 | 63 | func NewBuildCommand(name string) *BuildCommand { 64 | return &BuildCommand{ 65 | Name: name, 66 | RunIfConfig: RunIfConfigPassed, 67 | } 68 | } 69 | 70 | func NewBuild(id, locator, locatorForDisplay, 71 | consoleUrl, artifactUploadBaseUrl, propertyBaseUrl string, 72 | commands ...*BuildCommand) *Build { 73 | return &Build{ 74 | BuildId: id, 75 | BuildLocator: locator, 76 | BuildLocatorForDisplay: locator, 77 | ConsoleUrl: consoleUrl, 78 | ArtifactUploadBaseUrl: artifactUploadBaseUrl, 79 | PropertyBaseUrl: propertyBaseUrl, 80 | BuildCommand: ComposeCommand(commands...), 81 | } 82 | } 83 | 84 | func ComposeCommand(commands ...*BuildCommand) *BuildCommand { 85 | return NewBuildCommand(CommandCompose).AddCommands(commands...) 86 | } 87 | 88 | func CondCommand(commands ...*BuildCommand) *BuildCommand { 89 | return NewBuildCommand("cond").AddCommands(commands...) 90 | } 91 | 92 | func AndCommand(commands ...*BuildCommand) *BuildCommand { 93 | return NewBuildCommand("and").AddCommands(commands...) 94 | } 95 | 96 | func OrCommand(commands ...*BuildCommand) *BuildCommand { 97 | return NewBuildCommand("or").AddCommands(commands...) 98 | } 99 | 100 | func EchoCommand(line string) *BuildCommand { 101 | return NewBuildCommand(CommandEcho).AddArg("line", line) 102 | } 103 | 104 | func ExecCommand(args ...string) *BuildCommand { 105 | return NewBuildCommand(CommandExec).AddArg("command", args[0]).AddListArg("args", args[1:]) 106 | } 107 | 108 | func ExportCommand(kvs ...string) *BuildCommand { 109 | args := map[string]string{"name": kvs[0]} 110 | if len(kvs) == 3 { 111 | args["value"] = kvs[1] 112 | args["secure"] = kvs[2] 113 | } 114 | return NewBuildCommand(CommandExport).SetArgs(args) 115 | } 116 | 117 | func ReportCurrentStatusCommand(jobState string) *BuildCommand { 118 | args := map[string]string{"status": jobState} 119 | return NewBuildCommand(CommandReportCurrentStatus).SetArgs(args) 120 | } 121 | 122 | func ReportCompletingCommand() *BuildCommand { 123 | return NewBuildCommand(CommandReportCompleting).RunIf("any") 124 | } 125 | 126 | func TestCommand(args ...string) *BuildCommand { 127 | argsMap := map[string]string{ 128 | "flag": args[0], 129 | "left": args[1], 130 | } 131 | cmd := NewBuildCommand(CommandTest).SetArgs(argsMap) 132 | if len(args) > 2 { 133 | cmd.AddCommands(ExecCommand(args[2:]...)) 134 | } 135 | return cmd 136 | } 137 | 138 | func SecretCommand(vs ...string) *BuildCommand { 139 | args := map[string]string{"value": vs[0]} 140 | if len(vs) == 2 { 141 | args["substitution"] = vs[1] 142 | } 143 | return NewBuildCommand(CommandSecret).SetArgs(args) 144 | } 145 | 146 | func FailCommand(msg string) *BuildCommand { 147 | args := map[string]string{"message": msg} 148 | return NewBuildCommand(CommandFail).SetArgs(args) 149 | } 150 | 151 | func MkdirsCommand(path string) *BuildCommand { 152 | args := map[string]string{"path": path} 153 | return NewBuildCommand(CommandMkdirs).SetArgs(args) 154 | } 155 | 156 | func CleandirCommand(path string, allows ...string) *BuildCommand { 157 | return NewBuildCommand(CommandCleandir).AddArg("path", path).AddListArg("allowed", allows) 158 | } 159 | 160 | func UploadArtifactCommand(src, dest, ignoreUnmatchError string) *BuildCommand { 161 | args := map[string]string{ 162 | "src": src, 163 | "dest": dest, 164 | "ignoreUnmatchError": ignoreUnmatchError, 165 | } 166 | return NewBuildCommand(CommandUploadArtifact).SetArgs(args) 167 | } 168 | 169 | func DownloadFileCommand(src, url, dest, checksumUrl, checksumPath string) *BuildCommand { 170 | return DownloadCommand(CommandDownloadFile, src, url, dest, checksumUrl, checksumPath) 171 | } 172 | 173 | func DownloadDirCommand(src, url, dest, checksumUrl, checksumPath string) *BuildCommand { 174 | return DownloadCommand(CommandDownloadDir, src, url, dest, checksumUrl, checksumPath) 175 | } 176 | 177 | func DownloadCommand(file_or_dir, src, url, dest, checksumUrl, checksumPath string) *BuildCommand { 178 | args := map[string]string{ 179 | "src": src, 180 | "url": url, 181 | "dest": dest, 182 | "checksumUrl": checksumUrl, 183 | "checksumFile": checksumPath, 184 | } 185 | return NewBuildCommand(file_or_dir).SetArgs(args) 186 | } 187 | 188 | func GenerateTestReportCommand(args ...string) *BuildCommand { 189 | return NewBuildCommand(CommandGenerateTestReport).AddArg("uploadPath", args[0]).AddListArg("srcs", args[1:]) 190 | } 191 | 192 | func (cmd *BuildCommand) RunIfAny() bool { 193 | return strings.EqualFold(RunIfConfigAny, cmd.RunIfConfig) 194 | } 195 | 196 | func (cmd *BuildCommand) RunIfMatch(buildStatus string) bool { 197 | return strings.EqualFold(cmd.RunIfConfig, buildStatus) 198 | } 199 | 200 | func (cmd *BuildCommand) AddCommands(commands ...*BuildCommand) *BuildCommand { 201 | cmd.SubCommands = append(cmd.SubCommands, commands...) 202 | return cmd 203 | } 204 | 205 | func (cmd *BuildCommand) SetArgs(args map[string]string) *BuildCommand { 206 | cmd.Args = args 207 | return cmd 208 | } 209 | 210 | func (cmd *BuildCommand) AddArg(name, value string) *BuildCommand { 211 | if cmd.Args == nil { 212 | cmd.Args = make(map[string]string) 213 | } 214 | cmd.Args[name] = value 215 | return cmd 216 | } 217 | 218 | func (cmd *BuildCommand) AddListArg(name string, list []string) *BuildCommand { 219 | bs, err := json.Marshal(list) 220 | if err != nil { 221 | panic(err) 222 | } 223 | return cmd.AddArg(name, string(bs)) 224 | } 225 | 226 | func (cmd *BuildCommand) SetTest(test *BuildCommand) *BuildCommand { 227 | cmd.Test = test 228 | return cmd 229 | } 230 | 231 | func (cmd *BuildCommand) Setwd(wd string) *BuildCommand { 232 | cmd.WorkingDirectory = wd 233 | return cmd 234 | } 235 | 236 | func (cmd *BuildCommand) SetExecInput(input string) *BuildCommand { 237 | cmd.ExecInput = input 238 | return cmd 239 | } 240 | 241 | func (cmd *BuildCommand) RunIf(c string) *BuildCommand { 242 | cmd.RunIfConfig = c 243 | return cmd 244 | } 245 | 246 | func (cmd *BuildCommand) SetOnCancel(c *BuildCommand) *BuildCommand { 247 | cmd.OnCancel = c 248 | return cmd 249 | } 250 | 251 | func (cmd *BuildCommand) ListArg(name string) (list []string, err error) { 252 | err = json.Unmarshal([]byte(cmd.Args[name]), &list) 253 | return 254 | } 255 | -------------------------------------------------------------------------------- /protocol/build_command_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package protocol_test 18 | 19 | import ( 20 | . "github.com/gocd-contrib/gocd-golang-agent/protocol" 21 | "github.com/xli/assert" 22 | "testing" 23 | ) 24 | 25 | func TestListArg(t *testing.T) { 26 | cmd := NewBuildCommand("foo").AddListArg("lines", []string{"hello", "world", "!"}) 27 | list, err := cmd.ListArg("lines") 28 | assert.Nil(t, err) 29 | assert.Equal(t, 3, len(list)) 30 | assert.Equal(t, "hello", list[0]) 31 | assert.Equal(t, "world", list[1]) 32 | assert.Equal(t, "!", list[2]) 33 | 34 | assert.Equal(t, `["hello","world","!"]`, cmd.Args["lines"]) 35 | } 36 | 37 | func TestAddArg(t *testing.T) { 38 | cmd := NewBuildCommand(CommandCompose) 39 | cmd.AddArg("hello", "world") 40 | assert.Equal(t, "world", cmd.Args["hello"]) 41 | } 42 | 43 | func TestAddCommands(t *testing.T) { 44 | cmd := NewBuildCommand(CommandCompose) 45 | assert.Equal(t, 0, len(cmd.SubCommands)) 46 | cmd.AddCommands(NewBuildCommand(CommandEcho)) 47 | assert.Equal(t, 1, len(cmd.SubCommands)) 48 | } 49 | -------------------------------------------------------------------------------- /protocol/message.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package protocol 18 | 19 | import ( 20 | "encoding/json" 21 | "github.com/satori/go.uuid" 22 | ) 23 | 24 | const ( 25 | SetCookieAction = "setCookie" 26 | CancelBuildAction = "cancelBuild" 27 | ReregisterAction = "reregister" 28 | BuildAction = "build" 29 | PingAction = "ping" 30 | AckAction = "acknowledge" 31 | ReportCurrentStatusAction = "reportCurrentStatus" 32 | ReportCompletingAction = "reportCompleting" 33 | ReportCompletedAction = "reportCompleted" 34 | AssignWorkAction = "assignWork" 35 | ConsoleOutActon = "consoleOut" 36 | ) 37 | 38 | type Message struct { 39 | Action string `json:"action"` 40 | Data string `json:"data"` 41 | AcknowledgeId string `json:"acknowledgementId"` 42 | } 43 | 44 | func (m *Message) DataBuild() *Build { 45 | var build Build 46 | json.Unmarshal([]byte(m.Data), &build) 47 | return &build 48 | } 49 | 50 | func (m *Message) DataString() string { 51 | var str string 52 | json.Unmarshal([]byte(m.Data), &str) 53 | return str 54 | } 55 | 56 | func (m *Message) AgentRuntimeInfo() *AgentRuntimeInfo { 57 | var info AgentRuntimeInfo 58 | json.Unmarshal([]byte(m.Data), &info) 59 | return &info 60 | } 61 | 62 | func (m *Message) Report() *Report { 63 | var report Report 64 | json.Unmarshal([]byte(m.Data), &report) 65 | return &report 66 | } 67 | 68 | func newMessage(action string, data interface{}) *Message { 69 | json, err := json.Marshal(data) 70 | if err != nil { 71 | panic(err) 72 | } 73 | 74 | return &Message{ 75 | Action: action, 76 | Data: string(json), 77 | AcknowledgeId: uuid.NewV4().String(), 78 | } 79 | } 80 | 81 | func SetCookieMessage(cookie string) *Message { 82 | return newMessage(SetCookieAction, cookie) 83 | } 84 | 85 | func AckMessage(ackId string) *Message { 86 | return newMessage(AckAction, ackId) 87 | } 88 | 89 | func BuildMessage(cmd *Build) *Message { 90 | return newMessage(BuildAction, cmd) 91 | } 92 | 93 | func PingMessage(data *AgentRuntimeInfo) *Message { 94 | return newMessage(PingAction, data) 95 | } 96 | 97 | func ReportMessage(t string, report *Report) *Message { 98 | return newMessage(t, report) 99 | } 100 | 101 | func CompletedMessage(report *Report) *Message { 102 | return ReportMessage(ReportCompletedAction, report) 103 | } 104 | 105 | func ReregisterMessage() *Message { 106 | return &Message{Action: ReregisterAction} 107 | } 108 | 109 | func CancelMessage() *Message { 110 | return &Message{Action: CancelBuildAction} 111 | } 112 | -------------------------------------------------------------------------------- /protocol/registration.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package protocol 18 | 19 | type Registration struct { 20 | AgentPrivateKey, AgentCertificate string 21 | } 22 | -------------------------------------------------------------------------------- /protocol/report.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package protocol 18 | 19 | type Report struct { 20 | BuildId string `json:"buildId"` 21 | Result string `json:"result"` 22 | JobState string `json:"jobState"` 23 | AgentRuntimeInfo *AgentRuntimeInfo `json:"agentRuntimeInfo"` 24 | } 25 | -------------------------------------------------------------------------------- /protocol/websocket.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package protocol 18 | 19 | import ( 20 | "bytes" 21 | "compress/gzip" 22 | "encoding/json" 23 | "golang.org/x/net/websocket" 24 | "io/ioutil" 25 | ) 26 | 27 | func messageMarshal(v interface{}) ([]byte, byte, error) { 28 | json, jerr := json.Marshal(v) 29 | if jerr != nil { 30 | return []byte{}, websocket.BinaryFrame, jerr 31 | } 32 | var b bytes.Buffer 33 | w := gzip.NewWriter(&b) 34 | _, err := w.Write(json) 35 | w.Close() 36 | 37 | return b.Bytes(), websocket.BinaryFrame, err 38 | } 39 | 40 | func messageUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) { 41 | reader, _ := gzip.NewReader(bytes.NewBuffer(msg)) 42 | jsonBytes, _ := ioutil.ReadAll(reader) 43 | return json.Unmarshal(jsonBytes, v) 44 | } 45 | 46 | var messageCodec = websocket.Codec{messageMarshal, messageUnmarshal} 47 | 48 | func ReceiveMessage(conn *websocket.Conn) (*Message, error) { 49 | var msg Message 50 | err := messageCodec.Receive(conn, &msg) 51 | return &msg, err 52 | } 53 | 54 | func SendMessage(conn *websocket.Conn, msg *Message) error { 55 | return messageCodec.Send(conn, msg) 56 | } 57 | -------------------------------------------------------------------------------- /run_in_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 ThoughtWorks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | set -x 19 | set -e 20 | 21 | detectIP() { 22 | for i in 0 1 2 3 4 5 6 7 8 9 23 | do 24 | ip=`ipconfig getifaddr en${i}` 25 | if [ "${ip}" ] 26 | then 27 | echo $ip 28 | return 29 | fi 30 | done 31 | } 32 | 33 | CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w' . 34 | docker build -t gocd-golang-agent . 35 | 36 | GOCD_SERVER_HOST=${GOCD_SERVER_HOST:-`detectIP`} 37 | GOCD_SERVER_SSL_PORT=${GOCD_SERVER_SSL_PORT:-8154} 38 | 39 | docker run -e GOCD_SERVER_URL="https://$GOCD_SERVER_HOST:$GOCD_SERVER_SSL_PORT" -e DEBUG="${DEBUG}" gocd-golang-agent /gocd-golang-agent 40 | -------------------------------------------------------------------------------- /server/artifacts.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "archive/zip" 21 | "bytes" 22 | "io" 23 | "io/ioutil" 24 | "mime/multipart" 25 | "net/http" 26 | "os" 27 | "path/filepath" 28 | ) 29 | 30 | func artifactsHandler(s *Server) func(http.ResponseWriter, *http.Request) { 31 | return func(w http.ResponseWriter, req *http.Request) { 32 | switch req.Method { 33 | case http.MethodPost: 34 | handleArtifactsUpload(s, w, req) 35 | case http.MethodGet: 36 | handleArtifactDownload(s, w, req) 37 | default: 38 | w.WriteHeader(http.StatusMethodNotAllowed) 39 | } 40 | } 41 | } 42 | 43 | func handleArtifactDownload(s *Server, w http.ResponseWriter, req *http.Request) { 44 | buildId := parseBuildId(req.URL.Path) 45 | file := req.URL.Query()["file"] 46 | var fullPath string 47 | if len(file) == 1 { 48 | fullPath = s.ArtifactFile(buildId, file[0]) 49 | } else { 50 | fullPath = s.ChecksumFile(buildId) 51 | } 52 | info, err := os.Stat(fullPath) 53 | if err != nil { 54 | s.responseBadRequest(err, w) 55 | return 56 | } 57 | if info.IsDir() { 58 | s.log("Downloading directory %v", fullPath) 59 | zipfile, err := zipDirecotry(fullPath) 60 | if err != nil { 61 | s.responseBadRequest(err, w) 62 | return 63 | } 64 | f, err := os.Open(zipfile) 65 | if err != nil { 66 | s.responseBadRequest(err, w) 67 | return 68 | } 69 | defer f.Close() 70 | io.Copy(w, f) 71 | } else { 72 | s.log("Downloading %v", fullPath) 73 | f, err := os.Open(fullPath) 74 | if err != nil { 75 | s.responseBadRequest(err, w) 76 | return 77 | } 78 | defer f.Close() 79 | io.Copy(w, f) 80 | } 81 | } 82 | 83 | func handleArtifactsUpload(s *Server, w http.ResponseWriter, req *http.Request) { 84 | buildId := parseBuildId(req.URL.Path) 85 | form, err := req.MultipartReader() 86 | if err != nil { 87 | s.responseBadRequest(err, w) 88 | return 89 | } 90 | for { 91 | part, err := form.NextPart() 92 | if err == io.EOF { 93 | break 94 | } 95 | switch part.FormName() { 96 | case "zipfile": 97 | err = extractToArtifactDir(s, buildId, part) 98 | if err != nil { 99 | s.responseInternalError(err, w) 100 | return 101 | } 102 | case "file_checksum": 103 | bytes, err := ioutil.ReadAll(part) 104 | if err != nil { 105 | s.responseInternalError(err, w) 106 | return 107 | } 108 | err = s.appendToFile(s.ChecksumFile(buildId), bytes) 109 | if err != nil { 110 | s.responseInternalError(err, w) 111 | return 112 | } 113 | } 114 | } 115 | w.WriteHeader(http.StatusCreated) 116 | } 117 | 118 | func extractToArtifactDir(s *Server, buildId string, part *multipart.Part) error { 119 | // TODO: find out the right way to unzip multipart.Part in memory 120 | data, err := ioutil.ReadAll(part) 121 | if err != nil { 122 | return err 123 | } 124 | zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) 125 | if err != nil { 126 | return err 127 | } 128 | for _, file := range zipReader.File { 129 | dest := s.ArtifactFile(buildId, file.FileHeader.Name) 130 | err := extractArtifactFile(file, dest) 131 | if err != nil { 132 | return err 133 | } 134 | } 135 | return nil 136 | } 137 | 138 | func extractArtifactFile(file *zip.File, dest string) error { 139 | rc, err := file.Open() 140 | if err != nil { 141 | return err 142 | } 143 | defer rc.Close() 144 | 145 | err = os.MkdirAll(filepath.Dir(dest), 0755) 146 | if err != nil { 147 | return err 148 | } 149 | destFile, err := os.Create(dest) 150 | if err != nil { 151 | return err 152 | } 153 | defer destFile.Close() 154 | _, err = io.Copy(destFile, rc) 155 | return err 156 | } 157 | 158 | func zipDirecotry(source string) (string, error) { 159 | zipfile, err := ioutil.TempFile("", "tmp.zip") 160 | if err != nil { 161 | return "", err 162 | } 163 | defer zipfile.Close() 164 | w := zip.NewWriter(zipfile) 165 | defer w.Close() 166 | _, dirName := filepath.Split(source) 167 | err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error { 168 | if err != nil { 169 | return err 170 | } 171 | if info.IsDir() { 172 | return nil 173 | } 174 | 175 | file, err := os.Open(path) 176 | if err != nil { 177 | return err 178 | } 179 | defer file.Close() 180 | destFile := dirName + path[len(source):] 181 | writer, err := w.Create(destFile) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | _, err = io.Copy(writer, file) 187 | return err 188 | }) 189 | return zipfile.Name(), err 190 | 191 | } 192 | -------------------------------------------------------------------------------- /server/cert.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package server 17 | 18 | import ( 19 | "crypto/rand" 20 | "crypto/rsa" 21 | "crypto/x509" 22 | "crypto/x509/pkix" 23 | "encoding/pem" 24 | "math/big" 25 | "net" 26 | "os" 27 | "strings" 28 | "time" 29 | ) 30 | 31 | type Cert struct { 32 | Host string 33 | ValidFrom time.Time 34 | ValidFor time.Duration 35 | IsCA bool 36 | RsaBits int 37 | Organization string 38 | } 39 | 40 | func NewCert(host string) *Cert { 41 | return &Cert{ 42 | Host: host, 43 | ValidFrom: time.Now(), 44 | ValidFor: 365 * 24 * time.Hour, 45 | IsCA: true, 46 | RsaBits: 2048, 47 | Organization: "GoCD", 48 | } 49 | } 50 | 51 | func (c *Cert) publicKey(priv interface{}) interface{} { 52 | switch k := priv.(type) { 53 | case *rsa.PrivateKey: 54 | return &k.PublicKey 55 | default: 56 | return nil 57 | } 58 | } 59 | 60 | func (c *Cert) pemBlockForKey(priv interface{}) *pem.Block { 61 | switch k := priv.(type) { 62 | case *rsa.PrivateKey: 63 | return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)} 64 | default: 65 | return nil 66 | } 67 | } 68 | 69 | func (c *Cert) Generate(certFile, keyFile string) error { 70 | priv, err := rsa.GenerateKey(rand.Reader, c.RsaBits) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | notBefore := c.ValidFrom 76 | notAfter := notBefore.Add(c.ValidFor) 77 | 78 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 79 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | template := x509.Certificate{ 85 | SerialNumber: serialNumber, 86 | Subject: pkix.Name{ 87 | Organization: []string{c.Organization}, 88 | }, 89 | NotBefore: notBefore, 90 | NotAfter: notAfter, 91 | 92 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 93 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 94 | BasicConstraintsValid: true, 95 | } 96 | 97 | hosts := strings.Split(c.Host, ",") 98 | for _, h := range hosts { 99 | if ip := net.ParseIP(h); ip != nil { 100 | template.IPAddresses = append(template.IPAddresses, ip) 101 | } else { 102 | template.DNSNames = append(template.DNSNames, h) 103 | } 104 | } 105 | 106 | if c.IsCA { 107 | template.IsCA = true 108 | template.KeyUsage |= x509.KeyUsageCertSign 109 | } 110 | 111 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, c.publicKey(priv), priv) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | certOut, err := os.Create(certFile) 117 | if err != nil { 118 | return err 119 | } 120 | pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) 121 | certOut.Close() 122 | keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 123 | if err != nil { 124 | return err 125 | } 126 | pem.Encode(keyOut, c.pemBlockForKey(priv)) 127 | keyOut.Close() 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /server/console.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "io/ioutil" 21 | "net/http" 22 | ) 23 | 24 | func consoleHandler(s *Server) func(http.ResponseWriter, *http.Request) { 25 | return func(w http.ResponseWriter, req *http.Request) { 26 | buildId := parseBuildId(req.URL.Path) 27 | bytes, err := ioutil.ReadAll(req.Body) 28 | if err != nil { 29 | s.responseBadRequest(err, w) 30 | return 31 | } 32 | err = s.appendToFile(s.ConsoleLogFile(buildId), bytes) 33 | if err != nil { 34 | s.responseInternalError(err, w) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/filters.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "net/http" 21 | ) 22 | 23 | func (s *Server) LimittedRequestEntitySize(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { 24 | return func(w http.ResponseWriter, req *http.Request) { 25 | limit := s.MaxRequestEntitySize() 26 | if limit > 0 && req.ContentLength > limit { 27 | s.log("Request content length (%v) is larger than acceptable size (%d)", req.ContentLength, limit) 28 | w.WriteHeader(http.StatusRequestEntityTooLarge) 29 | return 30 | } 31 | handler(w, req) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/remote_agent.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "fmt" 21 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 22 | "github.com/satori/go.uuid" 23 | "golang.org/x/net/websocket" 24 | "io" 25 | ) 26 | 27 | type RemoteAgent struct { 28 | conn *websocket.Conn 29 | id string 30 | } 31 | 32 | func (agent *RemoteAgent) Listen(server *Server) error { 33 | for { 34 | msg, err := protocol.ReceiveMessage(agent.conn) 35 | if err == io.EOF { 36 | return err 37 | } else if err != nil { 38 | server.error("receive error: %v", err) 39 | } else { 40 | agent.processMessage(server, msg) 41 | } 42 | } 43 | } 44 | 45 | func (agent *RemoteAgent) processMessage(server *Server, msg *protocol.Message) { 46 | server.log("received message: %v", msg.Action) 47 | err := agent.Ack(msg) 48 | if err != nil { 49 | server.error("ack error: %v", err) 50 | } 51 | switch msg.Action { 52 | case protocol.PingAction: 53 | info := msg.AgentRuntimeInfo() 54 | if agent.id == "" { 55 | agent.id = info.Identifier.Uuid 56 | server.add(agent) 57 | agent.SetCookie() 58 | } 59 | agentState := info.RuntimeStatus 60 | server.notifyAgent(agent.id, agentState) 61 | case "reportCurrentStatus": 62 | report := msg.Report() 63 | server.notifyBuild(report.BuildId, report.JobState) 64 | case "reportCompleting", "reportCompleted": 65 | report := msg.Report() 66 | server.notifyBuild(report.BuildId, report.Result) 67 | } 68 | } 69 | 70 | func (agent *RemoteAgent) Send(msg *protocol.Message) error { 71 | return protocol.SendMessage(agent.conn, msg) 72 | } 73 | 74 | func (agent *RemoteAgent) SetCookie() error { 75 | return agent.Send(protocol.SetCookieMessage(uuid.NewV4().String())) 76 | } 77 | 78 | func (agent *RemoteAgent) Ack(msg *protocol.Message) error { 79 | if msg.AcknowledgeId != "" { 80 | return agent.Send(protocol.AckMessage(msg.AcknowledgeId)) 81 | } 82 | return nil 83 | } 84 | 85 | func (agent *RemoteAgent) String() string { 86 | return fmt.Sprintf("[agent %v, id: %v]", 87 | agent.conn.RemoteAddr(), agent.id) 88 | } 89 | 90 | func (agent *RemoteAgent) Close() error { 91 | return agent.conn.Close() 92 | } 93 | -------------------------------------------------------------------------------- /server/responses.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "net/http" 21 | ) 22 | 23 | func (s *Server) responseBadRequest(err error, w http.ResponseWriter) { 24 | s.log("Bad request: %v", err) 25 | w.WriteHeader(http.StatusBadRequest) 26 | } 27 | 28 | func (s *Server) responseInternalError(err error, w http.ResponseWriter) { 29 | s.error("Server internal error: %v", err) 30 | w.WriteHeader(http.StatusInternalServerError) 31 | } 32 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "encoding/json" 21 | "github.com/gocd-contrib/gocd-golang-agent/protocol" 22 | "golang.org/x/net/websocket" 23 | "io" 24 | "io/ioutil" 25 | "log" 26 | "net/http" 27 | "os" 28 | "path/filepath" 29 | "strings" 30 | "sync" 31 | ) 32 | 33 | const ( 34 | WebSocketPath = "/agent-websocket" 35 | RegistrationPath = "/agent-register" 36 | StatusPath = "/status" 37 | 38 | ConsoleLogPath = "/console" 39 | ArtifactsPath = "/artifacts" 40 | PropertiesPath = "/properties" 41 | ) 42 | 43 | type StateListener interface { 44 | Notify(class, id, state string) 45 | } 46 | 47 | type AgentMessage struct { 48 | agentId string 49 | Msg *protocol.Message 50 | } 51 | 52 | type Server struct { 53 | Address string 54 | CertPemFile string 55 | KeyPemFile string 56 | WorkingDir string 57 | Logger *log.Logger 58 | StateListeners []StateListener 59 | maxRequestEntitySize int64 60 | fieldChangeMu sync.Mutex 61 | 62 | addAgent chan *RemoteAgent 63 | delAgent chan *RemoteAgent 64 | sendMessage chan *AgentMessage 65 | } 66 | 67 | func New(address, certFile, keyFile, workingDir string, logger *log.Logger) *Server { 68 | return &Server{ 69 | Address: address, 70 | CertPemFile: certFile, 71 | KeyPemFile: keyFile, 72 | WorkingDir: workingDir, 73 | Logger: logger, 74 | addAgent: make(chan *RemoteAgent), 75 | delAgent: make(chan *RemoteAgent), 76 | sendMessage: make(chan *AgentMessage), 77 | } 78 | 79 | } 80 | 81 | func (s *Server) Start() error { 82 | go manageAgents(s) 83 | http.Handle(WebSocketPath, websocketHandler(s)) 84 | s.HandleFunc(RegistrationPath, registorHandler(s)) 85 | s.HandleFunc(ConsoleLogPath+"/", consoleHandler(s)) 86 | s.HandleFunc(ArtifactsPath+"/", artifactsHandler(s)) 87 | s.HandleFunc(StatusPath, statusHandler()) 88 | s.log("listen to %v", s.Address) 89 | return http.ListenAndServeTLS(s.Address, s.CertPemFile, s.KeyPemFile, nil) 90 | } 91 | 92 | func (s *Server) HandleFunc(path string, handler func(http.ResponseWriter, *http.Request)) { 93 | http.HandleFunc(path, 94 | s.LimittedRequestEntitySize(handler)) 95 | } 96 | 97 | func (s *Server) SendBuild(agentId, buildId string, commands ...*protocol.BuildCommand) { 98 | 99 | locator := "/builds/" + buildId 100 | build := protocol.NewBuild(buildId, locator, locator, 101 | ConsoleLogPath+locator, 102 | ArtifactsPath+locator, 103 | PropertiesPath+locator, 104 | commands...) 105 | s.Send(agentId, protocol.BuildMessage(build)) 106 | } 107 | 108 | func (s *Server) SetMaxRequestEntitySize(size int64) { 109 | s.fieldChangeMu.Lock() 110 | defer s.fieldChangeMu.Unlock() 111 | s.maxRequestEntitySize = size 112 | } 113 | 114 | func (s *Server) MaxRequestEntitySize() int64 { 115 | s.fieldChangeMu.Lock() 116 | defer s.fieldChangeMu.Unlock() 117 | return s.maxRequestEntitySize 118 | } 119 | 120 | func (s *Server) ConsoleLog(buildId string) (string, error) { 121 | bytes, err := ioutil.ReadFile(s.ConsoleLogFile(buildId)) 122 | return string(bytes), err 123 | } 124 | 125 | func (s *Server) Checksum(buildId string) (string, error) { 126 | bytes, err := ioutil.ReadFile(s.ChecksumFile(buildId)) 127 | return string(bytes), err 128 | } 129 | 130 | func (s *Server) ChecksumUrl(buildId string) string { 131 | return ArtifactsPath + "/builds/" + buildId 132 | } 133 | 134 | func (s *Server) ArtifactFile(buildId, file string) string { 135 | return filepath.Join(s.WorkingDir, buildId, "artifacts", file) 136 | } 137 | 138 | func (s *Server) ArtifactUrl(buildId, file string) string { 139 | return ArtifactsPath + "/builds/" + buildId + "?file=" + file 140 | } 141 | 142 | func (s *Server) ChecksumFile(buildId string) string { 143 | return filepath.Join(s.WorkingDir, buildId, "md5.checksum") 144 | } 145 | 146 | func (s *Server) ConsoleLogFile(buildId string) string { 147 | return filepath.Join(s.WorkingDir, buildId, "console.log") 148 | } 149 | 150 | func (s *Server) Send(agentId string, msg *protocol.Message) { 151 | s.sendMessage <- &AgentMessage{agentId: agentId, Msg: msg} 152 | } 153 | 154 | func (s *Server) log(format string, v ...interface{}) { 155 | s.Logger.Printf(format, v...) 156 | } 157 | 158 | func (s *Server) error(format string, v ...interface{}) { 159 | s.Logger.Printf(format, v...) 160 | } 161 | 162 | func (s *Server) add(agent *RemoteAgent) { 163 | s.addAgent <- agent 164 | } 165 | 166 | func (s *Server) del(agent *RemoteAgent) { 167 | s.delAgent <- agent 168 | } 169 | 170 | func (s *Server) notifyAgent(uuid, state string) { 171 | s.notify("agent", uuid, state) 172 | } 173 | 174 | func (s *Server) notifyBuild(uuid, state string) { 175 | s.notify("build", uuid, state) 176 | } 177 | 178 | func (s *Server) notify(class, uuid, state string) { 179 | for _, listener := range s.StateListeners { 180 | listener.Notify(class, uuid, state) 181 | } 182 | } 183 | 184 | func (s *Server) appendToFile(filename string, data []byte) error { 185 | err := os.MkdirAll(filepath.Dir(filename), 0755) 186 | if err != nil { 187 | return err 188 | } 189 | f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 190 | if err != nil { 191 | return err 192 | } 193 | s.log("append data(%v) to %v", len(data), filename) 194 | n, err := f.Write(data) 195 | if err == nil && n < len(data) { 196 | err = io.ErrShortWrite 197 | } 198 | if err1 := f.Close(); err == nil { 199 | err = err1 200 | } 201 | return err 202 | } 203 | 204 | func manageAgents(s *Server) { 205 | agents := make(map[string]*RemoteAgent) 206 | for { 207 | select { 208 | case agent := <-s.addAgent: 209 | agents[agent.id] = agent 210 | case agent := <-s.delAgent: 211 | delete(agents, agent.id) 212 | case am := <-s.sendMessage: 213 | agent := agents[am.agentId] 214 | if agent != nil { 215 | agent.Send(am.Msg) 216 | } else { 217 | s.log("could not find agent by id %v for sending message: %v", am.agentId, am.Msg.Action) 218 | } 219 | } 220 | } 221 | } 222 | 223 | func statusHandler() func(http.ResponseWriter, *http.Request) { 224 | return func(w http.ResponseWriter, req *http.Request) { 225 | w.Write([]byte("ok")) 226 | } 227 | } 228 | 229 | // todo: does not generate real agent cert and private key yet, just 230 | // use server cert and private key for testing environment. 231 | func registorHandler(s *Server) func(http.ResponseWriter, *http.Request) { 232 | return func(w http.ResponseWriter, req *http.Request) { 233 | var agentPrivateKey, agentCert, regJson []byte 234 | var err error 235 | var reg *protocol.Registration 236 | 237 | agentPrivateKey, err = ioutil.ReadFile(s.KeyPemFile) 238 | if err != nil { 239 | s.responseInternalError(err, w) 240 | return 241 | } 242 | agentCert, err = ioutil.ReadFile(s.CertPemFile) 243 | if err != nil { 244 | s.responseInternalError(err, w) 245 | return 246 | } 247 | 248 | reg = &protocol.Registration{ 249 | AgentPrivateKey: string(agentPrivateKey), 250 | AgentCertificate: string(agentCert), 251 | } 252 | regJson, err = json.Marshal(reg) 253 | if err != nil { 254 | s.responseInternalError(err, w) 255 | return 256 | } 257 | w.Write(regJson) 258 | } 259 | } 260 | 261 | func websocketHandler(s *Server) websocket.Handler { 262 | return websocket.Handler(func(ws *websocket.Conn) { 263 | agent := &RemoteAgent{conn: ws} 264 | s.log("websocket connection is open for %v", agent) 265 | err := agent.Listen(s) 266 | s.del(agent) 267 | if err != io.EOF { 268 | s.log("close websocket connection for %v", agent) 269 | err := agent.Close() 270 | if err != nil { 271 | s.error("error when closing websocket connection for %v: %v", agent, err) 272 | } 273 | } 274 | }) 275 | } 276 | 277 | func parseBuildId(path string) string { 278 | parts := strings.Split(path, "/") 279 | return parts[len(parts)-1] 280 | } 281 | -------------------------------------------------------------------------------- /start-agent.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | detectIP() { 6 | for i in 0 1 2 3 4 5 6 7 8 9 7 | do 8 | ip=`ipconfig getifaddr en${i}` 9 | if [ "${ip}" ] 10 | then 11 | echo $ip 12 | return 13 | fi 14 | done 15 | } 16 | 17 | GOCD_SERVER_HOST=${GOCD_SERVER_HOST:-`detectIP`} 18 | GOCD_SERVER_SSL_PORT=${GOCD_SERVER_SSL_PORT:-8154} 19 | 20 | docker run -e GOCD_SERVER_URL="https://$GOCD_SERVER_HOST:$GOCD_SERVER_SSL_PORT/go" -e DEBUG="${DEBUG}" -e GOCD_AGNENT_WORK_DIR="/var/lib/gocd-golang-agent" golang-agent /usr/bin/gocd-golang-agent 21 | -------------------------------------------------------------------------------- /stream/nop_closer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package stream 18 | 19 | import ( 20 | "io" 21 | ) 22 | 23 | type nopCloser struct { 24 | io.Writer 25 | } 26 | 27 | func (nopCloser) Close() error { return nil } 28 | 29 | func NopCloser(w io.Writer) io.WriteCloser { 30 | return nopCloser{w} 31 | } 32 | -------------------------------------------------------------------------------- /stream/prefix_writer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package stream 18 | 19 | import ( 20 | "bytes" 21 | "io" 22 | ) 23 | 24 | type PrefixWriter struct { 25 | io.Writer 26 | Prefix func() []byte 27 | ap bool 28 | } 29 | 30 | func NewPrefixWriter(writer io.Writer, prefix func() []byte) *PrefixWriter { 31 | return &PrefixWriter{writer, prefix, true} 32 | } 33 | 34 | func (w *PrefixWriter) Write(out []byte) (int, error) { 35 | if len(out) == 0 { 36 | return 0, nil 37 | } 38 | ln := []byte{'\n'} 39 | lines := bytes.Split(out, ln) 40 | last := len(lines) - 1 41 | for i, line := range lines { 42 | if i == last && len(line) == 0 { 43 | w.ap = true 44 | break 45 | } 46 | if i > 0 || w.ap { 47 | if err := w.appendPrefix(); err != nil { 48 | return -1, err 49 | } 50 | w.ap = false 51 | } 52 | w.Writer.Write(line) 53 | if i < last { 54 | w.Writer.Write(ln) 55 | } 56 | } 57 | return len(out), nil 58 | } 59 | 60 | func (w *PrefixWriter) appendPrefix() error { 61 | _, err := w.Writer.Write(w.Prefix()) 62 | return err 63 | } 64 | -------------------------------------------------------------------------------- /stream/prefix_writer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package stream_test 17 | 18 | import ( 19 | "bytes" 20 | . "github.com/gocd-contrib/gocd-golang-agent/stream" 21 | "github.com/xli/assert" 22 | "strconv" 23 | "testing" 24 | ) 25 | 26 | func TestPrefixWriter(t *testing.T) { 27 | var tests = []struct { 28 | inputs []string 29 | output string 30 | }{ 31 | {[]string{"hello"}, "1 hello"}, 32 | {[]string{"hello", " world"}, "2 hello world"}, 33 | {[]string{"hello\n", "world"}, "3 hello\n4 world"}, 34 | {[]string{"hello\nworld", "!"}, "5 hello\n6 world!"}, 35 | {[]string{"hello\nworld\n", "!"}, "7 hello\n8 world\n9 !"}, 36 | {[]string{"\n", "hello"}, "10 \n11 hello"}, 37 | {[]string{"\n", "\nhello"}, "12 \n13 \n14 hello"}, 38 | {[]string{"...", "...", "...", "\nhello"}, "15 .........\n16 hello"}, 39 | {[]string{"...", "...\n", "hello\n"}, "17 ......\n18 hello\n"}, 40 | {[]string{"hello\n"}, "19 hello\n"}, 41 | {[]string{"hello", "\n", "world"}, "20 hello\n21 world"}, 42 | } 43 | i := 0 44 | for _, test := range tests { 45 | var buf bytes.Buffer 46 | w := NewPrefixWriter(&buf, func() []byte { 47 | i++ 48 | return []byte(strconv.Itoa(i) + " ") 49 | }) 50 | for _, d := range test.inputs { 51 | size, err := w.Write([]byte(d)) 52 | assert.Nil(t, err) 53 | assert.Equal(t, len(d), size) 54 | } 55 | assert.Equal(t, test.output, buf.String()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /stream/substitute_writer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package stream 18 | 19 | import ( 20 | "io" 21 | "strings" 22 | ) 23 | 24 | type SubstituteWriter struct { 25 | io.Writer 26 | Substitutions map[string]interface{} 27 | } 28 | 29 | func NewSubstituteWriter(writer io.Writer) *SubstituteWriter { 30 | return &SubstituteWriter{writer, make(map[string]interface{})} 31 | } 32 | 33 | func (w *SubstituteWriter) Filter(writer io.Writer) *SubstituteWriter { 34 | return &SubstituteWriter{writer, w.Substitutions} 35 | } 36 | 37 | func (w *SubstituteWriter) Write(out []byte) (int, error) { 38 | str := string(out) 39 | for k, v := range w.Substitutions { 40 | vs, ok := v.(string) 41 | if !ok { 42 | f, _ := v.(func() string) 43 | vs = f() 44 | } 45 | str = strings.Replace(str, k, vs, -1) 46 | } 47 | 48 | _, err := w.Writer.Write([]byte(str)) 49 | return len(out), err 50 | } 51 | -------------------------------------------------------------------------------- /stream/substitute_writer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 ThoughtWorks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package stream_test 17 | 18 | import ( 19 | "bytes" 20 | . "github.com/gocd-contrib/gocd-golang-agent/stream" 21 | "github.com/xli/assert" 22 | "testing" 23 | ) 24 | 25 | func TestSubstituteWriter(t *testing.T) { 26 | var tests = []struct { 27 | subs map[string]interface{} 28 | inputs []string 29 | output string 30 | }{ 31 | { 32 | map[string]interface{}{ 33 | "${hello}": "world", 34 | }, 35 | []string{"hello ${hello}"}, 36 | "hello world", 37 | }, 38 | { 39 | map[string]interface{}{ 40 | "${hello}": "world", 41 | "abcd": "****", 42 | }, 43 | []string{"hello ${hello} ${abcd}", " ${hello}"}, 44 | "hello world ${****} world", 45 | }, 46 | { 47 | map[string]interface{}{ 48 | "${hello}": "world", 49 | "abcd": "****", 50 | }, 51 | []string{"hello ${hello} ${abcd}", " ${hello}"}, 52 | "hello world ${****} world", 53 | }, 54 | { 55 | map[string]interface{}{ 56 | "${hello}": func() string { return "world" }, 57 | }, 58 | []string{"hello ${hello}"}, 59 | "hello world", 60 | }, 61 | } 62 | for _, test := range tests { 63 | var buf bytes.Buffer 64 | w := &SubstituteWriter{ 65 | Substitutions: test.subs, 66 | Writer: &buf, 67 | } 68 | for _, d := range test.inputs { 69 | size, err := w.Write([]byte(d)) 70 | assert.Nil(t, err) 71 | assert.Equal(t, len(d), size) 72 | } 73 | assert.Equal(t, test.output, buf.String()) 74 | } 75 | } 76 | --------------------------------------------------------------------------------