From dfea14c7e53a33be04a3d56b6cddf99bfdac5408 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Tue, 29 Sep 2015 18:21:17 -0700 Subject: [PATCH] updated vendor files and paths --- .dockerignore | 13 - .drone.yml | 46 +- .gitignore | 41 +- Dockerfile | 36 +- Dockerfile.alpine | 26 - Makefile | 66 +- contrib/debian/README | 1 - contrib/generate-amber.go | 6 + contrib/generate-js.go | 60 + contrib/setup-sqlite.sh | 11 + controller/badge.go | 89 +- controller/badge_test.go | 104 - controller/build.go | 199 + controller/commits.go | 285 - controller/gitlab.go | 165 +- controller/{hooks.go => hook.go} | 155 +- controller/index.go | 199 + controller/login.go | 208 +- controller/node.go | 80 + controller/queue.go | 11 - controller/recorder/recorder.go | 33 - controller/repo.go | 274 + controller/repos.go | 349 - controller/server.go | 261 - controller/stream.go | 123 + controller/user.go | 127 +- controller/user_test.go | 88 - controller/users.go | 171 +- controller/ws.go | 105 - drone.go | 51 + engine/bus.go | 24 +- engine/bus_test.go | 47 +- engine/engine.go | 392 + engine/pool.go | 86 + engine/pool_test.go | 89 + engine/queue.go | 123 - engine/queue_test.go | 98 - engine/runner.go | 318 - engine/types.go | 24 + engine/updater.go | 75 +- engine/util.go | 35 + engine/worker.go | 140 +- make.go | 426 - model/build.go | 170 +- model/build_test.go | 207 + shared/ccmenu/ccmenu.go => model/cc.go | 24 +- model/cc_test.go | 83 + model/config.go | 126 - model/const.go | 18 + model/feed.go | 23 + model/hook.go | 8 - model/job.go | 67 +- model/job_test.go | 117 + model/key.go | 46 + model/key_test.go | 113 + model/log.go | 42 + model/log_test.go | 59 + model/netrc.go | 7 + model/node.go | 79 + model/node_test.go | 100 + model/perm.go | 7 + model/repo.go | 136 +- model/repo_test.go | 148 + model/star.go | 44 + model/star_test.go | 59 + model/status.go | 10 - model/sys.go | 8 + model/system.go | 27 - model/user.go | 127 +- model/user_test.go | 207 + model/util.go | 36 - model/util_test.go | 12 - remote/github/github.go | 436 + remote/github/github/github.go | 478 - remote/github/{github => }/helper.go | 6 +- remote/github/types.go | 48 + remote/gitlab/{gitlab => }/gitlab.go | 255 +- remote/gitlab/{gitlab => }/gitlab_test.go | 73 +- remote/gitlab/{gitlab => }/helper.go | 2 +- remote/gitlab/{gitlab => }/testdata/hooks.go | 0 remote/gitlab/{gitlab => }/testdata/oauth.go | 0 .../gitlab/{gitlab => }/testdata/projects.go | 0 .../gitlab/{gitlab => }/testdata/testdata.go | 0 remote/gitlab/{gitlab => }/testdata/users.go | 0 remote/remote.go | 83 +- router/middleware/context/context.go | 42 + router/middleware/header/header.go | 40 + router/middleware/session/repo.go | 236 + router/middleware/session/user.go | 98 + router/router.go | 179 + shared/crypto/crypto.go | 118 + .../crypto/crypto_test.go | 20 +- shared/crypto/sshutil/sshutil.go | 72 - shared/crypto/sshutil/sshutil_test.go | 40 - shared/database/database.go | 51 + shared/database/mysql/1_init.sql | 132 + shared/database/postgres/1_init.sql | 132 + shared/database/rebind.go | 32 + shared/database/sqlite3/1_init.sql | 131 + shared/docker/docker.go | 109 + shared/envconfig/envconfig.go | 117 + shared/httputil/httputil.go | 1 + shared/oauth2/oauth2.go | 471 + shared/server/server.go | 36 + shared/token/token.go | 48 +- shared/token/token_test.go | 1 - static/images/docker.svg | 70 + static/images/docker_blue.svg | 70 + static/images/docker_white.svg | 13 + static/images/favicon.ico | Bin 0 -> 1150 bytes static/images/favicon.png | Bin 0 -> 557 bytes static/images/logo_dark.svg | 93 + static/images/logo_light.svg | 93 + static/images/ubuntu.svg | 7 + static/scripts/build.js | 120 + static/scripts/csrf.js | 11 + static/scripts/models.js | 90 + static/scripts/nodes.js | 69 + static/scripts/repo.js | 148 + static/scripts/repos.js | 31 + static/scripts/search.js | 36 + static/scripts/term.js | 394 + static/scripts/term_buf.js | 52 + static/scripts/users.js | 78 + static/scripts/utils.js | 15 + static/scripts/vendor/livestamp.js | 138 + static/scripts/vendor/plates.js | 666 + static/scripts_gen/.gitkeep | 0 static/static.go | 26 + static/styles/header.sass | 0 static/styles/modules/badges.sass | 2 + static/styles/modules/navbar.sass | 56 + static/styles/modules/range.sass | 95 + static/styles/modules/status.sass | 46 + static/styles/modules/subnav.sass | 95 + static/styles/modules/switch.sass | 43 + static/styles/modules/timeline.sass | 86 + static/styles/pages/build.sass | 156 + static/styles/pages/login.sass | 87 + static/styles/pages/repo.sass | 101 + static/styles/pages/user.sass | 44 + static/styles/pages/users.sass | 71 + static/styles/search.sass | 37 + static/styles/style.sass | 106 + static/styles_gen/.gitkeep | 0 template/amber/400.amber | 8 + template/amber/401.amber | 8 + template/amber/403.amber | 8 + template/amber/404.amber | 8 + template/amber/500.amber | 8 + template/amber/_feed.amber | 71 + template/amber/_users.amber | 97 + template/amber/base.amber | 55 + template/amber/build.amber | 70 + template/amber/login.amber | 36 + template/amber/nodes.amber | 52 + template/amber/repo.amber | 50 + template/amber/repo_activate.amber | 33 + template/amber/repo_badge.amber | 37 + template/amber/repo_config.amber | 81 + template/amber/repo_secret.amber | 45 + template/amber/repos.amber | 13 + template/amber/user.amber | 33 + template/amber/users.amber | 40 + template/amber_gen/.gitkeep | 0 template/template.go | 57 + .../code.google.com/p/go.crypto/ssh/agent.go | 250 + .../p/go.crypto/ssh/agent/client.go | 563 - .../p/go.crypto/ssh/agent/client_test.go | 278 - .../p/go.crypto/ssh/agent/forward.go | 103 - .../p/go.crypto/ssh/agent/keyring.go | 183 - .../p/go.crypto/ssh/agent/server.go | 209 - .../p/go.crypto/ssh/agent/server_test.go | 77 - .../p/go.crypto/ssh/agent/testdata_test.go | 64 - .../p/go.crypto/ssh/benchmark_test.go | 122 - .../code.google.com/p/go.crypto/ssh/buffer.go | 12 +- .../p/go.crypto/ssh/buffer_test.go | 18 +- .../code.google.com/p/go.crypto/ssh/certs.go | 604 +- .../p/go.crypto/ssh/certs_test.go | 133 +- .../p/go.crypto/ssh/channel.go | 895 +- .../code.google.com/p/go.crypto/ssh/cipher.go | 286 +- .../p/go.crypto/ssh/cipher_test.go | 67 +- .../code.google.com/p/go.crypto/ssh/client.go | 578 +- .../p/go.crypto/ssh/client_auth.go | 348 +- .../p/go.crypto/ssh/client_auth_test.go | 477 +- .../p/go.crypto/ssh/client_test.go | 7 +- .../code.google.com/p/go.crypto/ssh/common.go | 281 +- .../p/go.crypto/ssh/common_test.go | 57 + .../p/go.crypto/ssh/connection.go | 144 - vendor/code.google.com/p/go.crypto/ssh/doc.go | 1 + .../p/go.crypto/ssh/example_test.go | 95 +- .../p/go.crypto/ssh/handshake.go | 393 - .../p/go.crypto/ssh/handshake_test.go | 311 - vendor/code.google.com/p/go.crypto/ssh/kex.go | 24 +- .../p/go.crypto/ssh/kex_test.go | 2 +- .../code.google.com/p/go.crypto/ssh/keys.go | 369 +- .../p/go.crypto/ssh/keys_test.go | 292 +- vendor/code.google.com/p/go.crypto/ssh/mac.go | 5 + .../p/go.crypto/ssh/mempipe_test.go | 22 +- .../p/go.crypto/ssh/messages.go | 315 +- .../p/go.crypto/ssh/messages_test.go | 144 +- vendor/code.google.com/p/go.crypto/ssh/mux.go | 356 - .../p/go.crypto/ssh/mux_test.go | 525 - .../code.google.com/p/go.crypto/ssh/server.go | 675 +- .../p/go.crypto/ssh/server_terminal.go | 81 + .../p/go.crypto/ssh/session.go | 319 +- .../p/go.crypto/ssh/session_test.go | 413 +- .../code.google.com/p/go.crypto/ssh/tcpip.go | 195 +- .../p/go.crypto/ssh/tcpip_test.go | 4 - .../p/go.crypto/ssh/terminal/terminal.go | 311 +- .../p/go.crypto/ssh/terminal/terminal_test.go | 34 - .../p/go.crypto/ssh/terminal/util.go | 2 +- .../p/go.crypto/ssh/terminal/util_bsd.go | 2 +- .../p/go.crypto/ssh/terminal/util_linux.go | 11 +- .../p/go.crypto/ssh/terminal/util_windows.go | 174 - .../p/go.crypto/ssh/test/agent_unix_test.go | 50 - .../p/go.crypto/ssh/test/cert_test.go | 47 - .../p/go.crypto/ssh/test/forward_unix_test.go | 2 +- .../p/go.crypto/ssh/test/keys_test.go | 246 + .../p/go.crypto/ssh/test/session_test.go | 146 +- .../p/go.crypto/ssh/test/tcpip_test.go | 49 +- .../p/go.crypto/ssh/test/test_unix_test.go | 145 +- .../p/go.crypto/ssh/test/testdata_test.go | 64 - .../p/go.crypto/ssh/testdata/doc.go | 8 - .../p/go.crypto/ssh/testdata/keys.go | 43 - .../p/go.crypto/ssh/testdata_test.go | 63 - .../p/go.crypto/ssh/transport.go | 337 +- .../p/go.crypto/ssh/transport_test.go | 40 - .../Bugagazavr/go-gitlab-client/.gitignore | 3 - .../Bugagazavr/go-gitlab-client/.travis.yml | 6 - .../go-gitlab-client/gitlab_test.go | 2 +- .../go-gitlab-client/hook_payload_test.go | 2 +- .../Bugagazavr/go-gitlab-client/hooks_test.go | 2 +- .../go-gitlab-client/projects_test.go | 2 +- .../go-gitlab-client/public_keys_test.go | 2 +- .../go-gitlab-client/repositories_test.go | 2 +- .../go-gitlab-client/session_test.go | 2 +- .../Bugagazavr/go-gitlab-client/users_test.go | 2 +- .../Bugagazavr/go-gitlab-client/util_test.go | 2 +- .../github.com/BurntSushi/migration/Makefile | 7 - .../github.com/BurntSushi/migration/README.md | 40 - .../github.com/BurntSushi/migration/UNLICENSE | 24 - vendor/github.com/BurntSushi/migration/doc.go | 20 - .../BurntSushi/migration/migration.go | 237 - .../BurntSushi/migration/session.vim | 1 - vendor/github.com/Sirupsen/logrus/.gitignore | 1 - vendor/github.com/Sirupsen/logrus/.travis.yml | 8 - vendor/github.com/Sirupsen/logrus/README.md | 3 - .../github.com/Sirupsen/logrus/entry_test.go | 2 +- .../Sirupsen/logrus/examples/basic/basic.go | 12 +- .../Sirupsen/logrus/examples/hook/hook.go | 4 +- vendor/github.com/Sirupsen/logrus/exported.go | 2 - .../github.com/Sirupsen/logrus/hook_test.go | 2 +- .../logrus/hooks/airbrake/airbrake.go | 4 +- .../logrus/hooks/airbrake/airbrake_test.go | 57 - .../logrus/hooks/papertrail/papertrail.go | 2 +- .../hooks/papertrail/papertrail_test.go | 2 +- .../Sirupsen/logrus/hooks/sentry/README.md | 2 +- .../Sirupsen/logrus/hooks/sentry/sentry.go | 2 +- .../logrus/hooks/sentry/sentry_test.go | 2 +- .../Sirupsen/logrus/hooks/syslog/syslog.go | 2 +- .../logrus/hooks/syslog/syslog_test.go | 2 +- .../Sirupsen/logrus/json_formatter.go | 8 +- .../Sirupsen/logrus/json_formatter_test.go | 120 - .../github.com/Sirupsen/logrus/logrus_test.go | 20 +- .../Sirupsen/logrus/text_formatter.go | 39 +- .../Sirupsen/logrus/text_formatter_test.go | 4 - vendor/github.com/codegangsta/cli/LICENSE | 21 + vendor/github.com/codegangsta/cli/README.md | 298 + vendor/github.com/codegangsta/cli/app.go | 275 + vendor/github.com/codegangsta/cli/app_test.go | 467 + .../cli/autocomplete/bash_autocomplete | 13 + .../cli/autocomplete/zsh_autocomplete | 5 + vendor/github.com/codegangsta/cli/cli.go | 19 + vendor/github.com/codegangsta/cli/cli_test.go | 100 + vendor/github.com/codegangsta/cli/command.go | 144 + .../codegangsta/cli/command_test.go | 49 + vendor/github.com/codegangsta/cli/context.go | 339 + .../codegangsta/cli/context_test.go | 99 + vendor/github.com/codegangsta/cli/flag.go | 447 + .../github.com/codegangsta/cli/flag_test.go | 743 + vendor/github.com/codegangsta/cli/help.go | 211 + .../codegangsta/cli/helpers_test.go | 19 + .../denisenkom/go-mssqldb/README.md | 83 + .../github.com/denisenkom/go-mssqldb/buf.go | 212 + .../denisenkom/go-mssqldb/charset.go | 113 + .../denisenkom/go-mssqldb/collation.go | 39 + .../denisenkom/go-mssqldb/cp1250.go | 262 + .../denisenkom/go-mssqldb/cp1251.go | 262 + .../denisenkom/go-mssqldb/cp1252.go | 262 + .../denisenkom/go-mssqldb/cp1253.go | 262 + .../denisenkom/go-mssqldb/cp1254.go | 262 + .../denisenkom/go-mssqldb/cp1255.go | 262 + .../denisenkom/go-mssqldb/cp1256.go | 262 + .../denisenkom/go-mssqldb/cp1257.go | 262 + .../denisenkom/go-mssqldb/cp1258.go | 262 + .../github.com/denisenkom/go-mssqldb/cp437.go | 262 + .../github.com/denisenkom/go-mssqldb/cp850.go | 262 + .../github.com/denisenkom/go-mssqldb/cp874.go | 262 + .../github.com/denisenkom/go-mssqldb/cp932.go | 7988 ++++++ .../github.com/denisenkom/go-mssqldb/cp936.go | 22055 ++++++++++++++++ .../github.com/denisenkom/go-mssqldb/cp949.go | 17312 ++++++++++++ .../github.com/denisenkom/go-mssqldb/cp950.go | 13767 ++++++++++ .../denisenkom/go-mssqldb/decimal.go | 115 + .../denisenkom/go-mssqldb/decimal_test.go | 82 + .../github.com/denisenkom/go-mssqldb/error.go | 39 + .../denisenkom/go-mssqldb/examples/simple.go | 53 + .../denisenkom/go-mssqldb/examples/tsql.go | 119 + .../github.com/denisenkom/go-mssqldb/log.go | 23 + .../github.com/denisenkom/go-mssqldb/mssql.go | 472 + .../denisenkom/go-mssqldb/mssql_go1.3.go | 11 + .../denisenkom/go-mssqldb/mssql_go1.3pre.go | 11 + .../github.com/denisenkom/go-mssqldb/net.go | 99 + .../github.com/denisenkom/go-mssqldb/ntlm.go | 283 + .../denisenkom/go-mssqldb/ntlm_test.go | 76 + .../denisenkom/go-mssqldb/parser.go | 227 + .../denisenkom/go-mssqldb/parser_test.go | 50 + .../denisenkom/go-mssqldb/queries_test.go | 685 + .../github.com/denisenkom/go-mssqldb/rpc.go | 100 + .../denisenkom/go-mssqldb/sspi_windows.go | 266 + .../github.com/denisenkom/go-mssqldb/tds.go | 995 + .../denisenkom/go-mssqldb/tds_test.go | 336 + .../github.com/denisenkom/go-mssqldb/token.go | 432 + .../github.com/denisenkom/go-mssqldb/tran.go | 99 + .../github.com/denisenkom/go-mssqldb/types.go | 847 + vendor/github.com/dgrijalva/jwt-go/.gitignore | 4 - .../github.com/dgrijalva/jwt-go/.travis.yml | 7 - .../docker/docker/pkg/stdcopy/stdcopy.go | 46 +- .../docker/docker/pkg/stdcopy/stdcopy_test.go | 85 + .../docker/docker/pkg/units/duration.go | 31 + .../docker/docker/pkg/units/duration_test.go | 46 + .../docker/docker/pkg/units/size.go | 93 + .../docker/docker/pkg/units/size_test.go | 108 + .../envconfig => dustin/go-broadcast}/LICENSE | 2 +- .../dustin/go-broadcast/README.markdown | 5 + .../dustin/go-broadcast/broadcaster.go | 86 + .../dustin/go-broadcast/broadcaster_test.go | 100 + .../dustin/go-broadcast/mux_observer.go | 133 + .../dustin/go-broadcast/mux_observer_test.go | 67 + vendor/github.com/eknkc/amber/README.md | 424 + vendor/github.com/eknkc/amber/amber_test.go | 313 + vendor/github.com/eknkc/amber/amberc/cli.go | 48 + vendor/github.com/eknkc/amber/compiler.go | 777 + vendor/github.com/eknkc/amber/doc.go | 257 + vendor/github.com/eknkc/amber/parser/nodes.go | 281 + .../github.com/eknkc/amber/parser/parser.go | 454 + .../github.com/eknkc/amber/parser/scanner.go | 501 + vendor/github.com/eknkc/amber/runtime.go | 287 + .../eknkc/amber/samples/basic.amber | 28 + .../amber/samples/compiledir_test/basic.amber | 28 + .../compiledir_test/basic.amber | 28 + .../eknkc/amber/samples/inherit.amber | 11 + .../eknkc/amber/samples/inherit.master.amber | 16 + .../samples/multilevel.inheritance.a.amber | 2 + .../samples/multilevel.inheritance.b.amber | 4 + .../samples/multilevel.inheritance.c.amber | 4 + vendor/github.com/franela/goblin/.gitignore | 22 - vendor/github.com/franela/goblin/.travis.yml | 8 - vendor/github.com/franela/goblin/Makefile | 3 - vendor/github.com/franela/goblin/README.md | 132 - .../github.com/franela/goblin/assertions.go | 59 - .../franela/goblin/assertions_test.go | 101 - .../franela/goblin/describe_test.go | 173 - vendor/github.com/franela/goblin/goblin.go | 280 - .../github.com/franela/goblin/goblin_logo.jpg | Bin 36893 -> 0 bytes .../franela/goblin/goblin_output.png | Bin 18985 -> 0 bytes .../github.com/franela/goblin/goblin_test.go | 269 - vendor/github.com/franela/goblin/it_test.go | 174 - .../franela/goblin/mono_reporter.go | 26 - vendor/github.com/franela/goblin/reporting.go | 137 - .../franela/goblin/reporting_test.go | 163 - vendor/github.com/franela/goblin/resolver.go | 21 - .../franela/goblin/resolver_test.go | 20 - .../getsentry/raven-go/Dockerfile.test | 11 + .../flag => getsentry/raven-go}/LICENSE | 7 +- .../github.com/getsentry/raven-go/README.md | 12 + .../github.com/getsentry/raven-go/client.go | 683 + .../getsentry/raven-go/client_test.go | 150 + .../getsentry/raven-go/docs/Makefile | 153 + .../getsentry/raven-go/docs/conf.py | 248 + .../getsentry/raven-go/docs/index.rst | 41 + .../getsentry/raven-go/docs/make.bat | 190 + .../raven-go/docs/sentry-doc-config.json | 15 + .../getsentry/raven-go/example/example.go | 42 + .../getsentry/raven-go/examples_test.go | 28 + .../getsentry/raven-go/exception.go | 41 + .../getsentry/raven-go/exception_test.go | 29 + vendor/github.com/getsentry/raven-go/http.go | 84 + .../getsentry/raven-go/http_test.go | 149 + .../getsentry/raven-go/interfaces.go | 49 + .../github.com/getsentry/raven-go/runtests.sh | 4 + .../getsentry/raven-go/stacktrace.go | 205 + .../getsentry/raven-go/stacktrace_test.go | 133 + .../github.com/getsentry/raven-go/writer.go | 20 + vendor/github.com/gin-gonic/gin/.gitignore | 4 - vendor/github.com/gin-gonic/gin/.travis.yml | 20 - vendor/github.com/gin-gonic/gin/BENCHMARKS.md | 298 + vendor/github.com/gin-gonic/gin/CHANGELOG.md | 48 +- .../gin-gonic/gin/Godeps/Godeps.json | 26 +- vendor/github.com/gin-gonic/gin/README.md | 447 +- vendor/github.com/gin-gonic/gin/auth.go | 16 +- vendor/github.com/gin-gonic/gin/auth_test.go | 2 +- .../gin-gonic/gin/benchmarks_test.go | 157 + .../gin-gonic/gin/binding/binding.go | 35 +- .../gin-gonic/gin/binding/binding_test.go | 80 +- .../gin/binding/default_validator.go | 40 + .../github.com/gin-gonic/gin/binding/form.go | 39 +- .../gin-gonic/gin/binding/form_mapping.go | 15 +- .../github.com/gin-gonic/gin/binding/json.go | 6 +- .../gin-gonic/gin/binding/validate_test.go | 20 +- .../github.com/gin-gonic/gin/binding/xml.go | 6 +- vendor/github.com/gin-gonic/gin/context.go | 370 +- .../github.com/gin-gonic/gin/context_test.go | 295 +- vendor/github.com/gin-gonic/gin/debug.go | 43 +- vendor/github.com/gin-gonic/gin/debug_test.go | 28 +- vendor/github.com/gin-gonic/gin/errors.go | 131 +- .../github.com/gin-gonic/gin/errors_test.go | 99 + .../gin/examples/app-engine/README.md | 7 + .../gin/examples/app-engine/app.yaml | 8 + .../gin/examples/app-engine/hello.go | 23 + .../gin-gonic/gin/examples/basic/main.go | 56 + .../gin/examples/realtime-advanced/main.go | 39 + .../resources/static/realtime.js | 144 + .../gin/examples/realtime-advanced/rooms.go | 25 + .../gin/examples/realtime-advanced/routes.go | 96 + .../gin/examples/realtime-advanced/stats.go | 56 + .../gin/examples/realtime-chat/main.go | 58 + .../gin/examples/realtime-chat/rooms.go | 33 + .../gin/examples/realtime-chat/template.go | 44 + vendor/github.com/gin-gonic/gin/fs.go | 42 + vendor/github.com/gin-gonic/gin/gin.go | 251 +- .../gin-gonic/gin/gin_integration_test.go | 105 + vendor/github.com/gin-gonic/gin/gin_test.go | 191 +- .../gin-gonic/gin/githubapi_test.go | 55 +- vendor/github.com/gin-gonic/gin/logger.go | 15 +- .../github.com/gin-gonic/gin/logger_test.go | 95 +- .../gin-gonic/gin/middleware_test.go | 141 +- vendor/github.com/gin-gonic/gin/mode.go | 18 +- vendor/github.com/gin-gonic/gin/mode_test.go | 2 +- vendor/github.com/gin-gonic/gin/path.go | 2 +- vendor/github.com/gin-gonic/gin/path_test.go | 10 +- .../github.com/gin-gonic/gin/recovery_test.go | 2 +- .../github.com/gin-gonic/gin/render/data.go | 8 +- .../github.com/gin-gonic/gin/render/file.go | 13 - .../github.com/gin-gonic/gin/render/html.go | 18 +- .../github.com/gin-gonic/gin/render/json.go | 20 +- .../gin-gonic/gin/render/redirect.go | 6 +- .../github.com/gin-gonic/gin/render/render.go | 10 +- .../gin-gonic/gin/render/render_test.go | 14 +- .../github.com/gin-gonic/gin/render/text.go | 27 +- vendor/github.com/gin-gonic/gin/render/xml.go | 10 +- .../gin-gonic/gin/response_writer.go | 21 + .../gin-gonic/gin/response_writer_test.go | 2 +- .../github.com/gin-gonic/gin/routergroup.go | 171 +- .../gin-gonic/gin/routergroup_test.go | 98 +- .../github.com/gin-gonic/gin/routes_test.go | 204 +- vendor/github.com/gin-gonic/gin/tree.go | 43 + vendor/github.com/gin-gonic/gin/utils.go | 37 + vendor/github.com/gin-gonic/gin/utils_test.go | 29 +- .../github.com/go-sql-driver/mysql/.gitignore | 8 - .../go-sql-driver/mysql/.travis.yml | 7 - .../google/go-github/github/github.go | 2 +- .../google/go-github/github/search.go | 2 +- .../gorilla/securecookie/.travis.yml | 7 - .../gorilla/securecookie/securecookie.go | 22 +- .../gorilla/securecookie/securecookie_test.go | 32 - .../hashicorp/golang-lru/.gitignore | 23 - .../github.com/hashicorp/golang-lru/README.md | 2 +- vendor/github.com/hashicorp/golang-lru/lru.go | 59 +- .../hashicorp/golang-lru/lru_test.go | 49 +- vendor/github.com/lib/pq/.gitignore | 4 - vendor/github.com/lib/pq/.travis.yml | 62 - vendor/github.com/lib/pq/README.md | 10 - vendor/github.com/lib/pq/bench_test.go | 5 +- vendor/github.com/lib/pq/buf.go | 3 +- vendor/github.com/lib/pq/certs/README | 3 - vendor/github.com/lib/pq/certs/postgresql.crt | 69 - vendor/github.com/lib/pq/certs/postgresql.key | 15 - vendor/github.com/lib/pq/certs/root.crt | 24 - vendor/github.com/lib/pq/certs/server.crt | 81 - vendor/github.com/lib/pq/certs/server.key | 27 - vendor/github.com/lib/pq/conn.go | 396 +- vendor/github.com/lib/pq/conn_test.go | 139 +- vendor/github.com/lib/pq/conn_xact_test.go | 61 + vendor/github.com/lib/pq/copy.go | 67 +- vendor/github.com/lib/pq/copy_test.go | 138 - vendor/github.com/lib/pq/doc.go | 9 +- vendor/github.com/lib/pq/encode.go | 141 +- vendor/github.com/lib/pq/encode_test.go | 205 +- vendor/github.com/lib/pq/error.go | 23 +- .../github.com/lib/pq/hstore/hstore_test.go | 6 +- .../github.com/lib/pq/listen_example/doc.go | 4 +- vendor/github.com/lib/pq/notify.go | 54 +- vendor/github.com/lib/pq/notify_test.go | 84 +- vendor/github.com/lib/pq/oid/gen.go | 4 +- vendor/github.com/lib/pq/ssl_test.go | 226 - vendor/github.com/lib/pq/url.go | 2 +- vendor/github.com/lib/pq/user_posix.go | 19 +- vendor/github.com/lib/pq/user_windows.go | 2 +- .../github.com/manucorporat/sse/.travis.yml | 6 - .../LICENSE.md => manucorporat/sse/LICENSE} | 12 +- .../manucorporat/sse/sse-decoder.go | 115 + .../manucorporat/sse/sse-decoder_test.go | 116 + .../manucorporat/sse/sse-encoder.go | 70 +- .../github.com/manucorporat/sse/sse_test.go | 132 +- vendor/github.com/manucorporat/sse/writer.go | 24 + vendor/github.com/manucorporat/stats/stats.go | 87 + .../github.com/mattn/go-colorable/README.md | 1 + vendor/github.com/mattn/go-sqlite3/.gitignore | 3 - .../github.com/mattn/go-sqlite3/.travis.yml | 9 - vendor/github.com/mattn/go-sqlite3/README.md | 8 +- vendor/github.com/mattn/go-sqlite3/backup.go | 2 +- .../{sqlite3-binding.c => sqlite3.c} | 0 vendor/github.com/mattn/go-sqlite3/sqlite3.go | 178 +- .../{sqlite3-binding.h => sqlite3.h} | 0 .../mattn/go-sqlite3/sqlite3_other.go | 1 + .../mattn/go-sqlite3/sqlite3_test.go | 318 +- .../mattn/go-sqlite3/sqlite3_windows.go | 1 - .../github.com/mattn/go-sqlite3/sqlite3ext.h | 2 +- vendor/github.com/mitchellh/cli/LICENSE | 354 + vendor/github.com/mitchellh/cli/README.md | 56 + vendor/github.com/mitchellh/cli/cli.go | 157 + vendor/github.com/mitchellh/cli/cli_test.go | 177 + vendor/github.com/mitchellh/cli/command.go | 23 + .../github.com/mitchellh/cli/command_mock.go | 30 + .../mitchellh/cli/command_mock_test.go | 9 + vendor/github.com/mitchellh/cli/help.go | 59 + vendor/github.com/mitchellh/cli/ui.go | 140 + vendor/github.com/mitchellh/cli/ui_colored.go | 60 + .../mitchellh/cli/ui_colored_test.go | 61 + .../github.com/mitchellh/cli/ui_concurrent.go | 40 + .../mitchellh/cli/ui_concurrent_test.go | 9 + vendor/github.com/mitchellh/cli/ui_mock.go | 53 + .../github.com/mitchellh/cli/ui_mock_test.go | 9 + vendor/github.com/mitchellh/cli/ui_test.go | 112 + vendor/github.com/mitchellh/cli/ui_writer.go | 18 + .../mitchellh/cli/ui_writer_test.go | 24 + vendor/github.com/namsral/flag/README.md | 184 - .../github.com/namsral/flag/example_test.go | 82 - .../namsral/flag/examples/gopher.conf | 5 - .../namsral/flag/examples/gopher.go | 29 - vendor/github.com/namsral/flag/export_test.go | 17 - vendor/github.com/namsral/flag/flag.go | 1040 - vendor/github.com/namsral/flag/flag_test.go | 475 - .../namsral/flag/testdata/test.conf | 11 - .../olekukonko/tablewriter/LICENCE.md | 19 + .../olekukonko/tablewriter/README.md | 141 + .../github.com/olekukonko/tablewriter/csv.go | 52 + .../tablewriter/csv2table/README.md | 43 + .../tablewriter/csv2table/csv2table.go | 84 + .../olekukonko/tablewriter/table.go | 472 + .../olekukonko/tablewriter/table_test.go | 276 + .../olekukonko/tablewriter/test.csv | 4 + .../olekukonko/tablewriter/test_info.csv | 4 + .../github.com/olekukonko/tablewriter/util.go | 73 + .../github.com/olekukonko/tablewriter/wrap.go | 103 + .../olekukonko/tablewriter/wrap_test.go | 44 + .../github.com/rubenv/sql-migrate/README.md | 245 + .../rubenv/sql-migrate/bindata_test.go | 136 + vendor/github.com/rubenv/sql-migrate/doc.go | 199 + .../rubenv/sql-migrate/init_test.go | 9 + .../github.com/rubenv/sql-migrate/migrate.go | 475 + .../rubenv/sql-migrate/migrate_test.go | 357 + .../rubenv/sql-migrate/sort_test.go | 34 + .../sql-migrate/sql-migrate/command_common.go | 63 + .../sql-migrate/sql-migrate/command_down.go | 55 + .../sql-migrate/sql-migrate/command_redo.go | 88 + .../sql-migrate/sql-migrate/command_status.go | 113 + .../sql-migrate/sql-migrate/command_up.go | 55 + .../rubenv/sql-migrate/sql-migrate/config.go | 103 + .../rubenv/sql-migrate/sql-migrate/main.go | 46 + .../sql-migrate/sql-migrate/main_test.go | 1 + .../rubenv/sql-migrate/sql-migrate/mssql.go | 12 + .../rubenv/sql-migrate/sqlparse/README.md | 28 + .../rubenv/sql-migrate/sqlparse/sqlparse.go | 128 + .../sql-migrate/sqlparse/sqlparse_test.go | 151 + .../sql-migrate/test-integration/dbconfig.yml | 20 + .../test-integration/mysql-flag.sh | 10 + .../sql-migrate/test-integration/mysql.sh | 14 + .../sql-migrate/test-integration/postgres.sh | 14 + .../sql-migrate/test-integration/sqlite.sh | 17 + .../sql-migrate/test-migrations/1_initial.sql | 8 + .../sql-migrate/test-migrations/2_record.sql | 5 + .../rubenv/sql-migrate/toapply_test.go | 101 + vendor/github.com/russross/meddler/.gitignore | 1 - vendor/github.com/russross/meddler/meddler.go | 4 - .../github.com/russross/meddler/scan_test.go | 2 +- .../samalba/dockerclient/.gitignore | 22 - .../github.com/samalba/dockerclient/README.md | 45 +- .../github.com/samalba/dockerclient/auth.go | 23 +- .../samalba/dockerclient/auth_test.go | 2 +- .../samalba/dockerclient/dockerclient.go | 383 +- .../samalba/dockerclient/dockerclient_test.go | 87 + .../samalba/dockerclient/engine_mock_test.go | 41 +- .../samalba/dockerclient/example_responses.go | 2 + .../samalba/dockerclient/examples/events.go | 11 +- .../dockerclient/examples/stats/stats.go | 43 + .../samalba/dockerclient/interface.go | 21 +- .../samalba/dockerclient/mockclient/mock.go | 65 +- .../dockerclient/mockclient/mock_test.go | 2 +- .../github.com/samalba/dockerclient/types.go | 356 +- vendor/github.com/square/go-jose/.gitignore | 7 - vendor/github.com/square/go-jose/.travis.yml | 36 - .../github.com/square/go-jose/asymmetric.go | 2 +- .../square/go-jose/jose-util/main.go | 2 +- vendor/github.com/square/go-jose/symmetric.go | 2 +- vendor/github.com/stretchr/objx/.gitignore | 22 - vendor/github.com/stretchr/objx/README.md | 3 - vendor/github.com/stretchr/objx/accessors.go | 179 - .../stretchr/objx/accessors_test.go | 145 - .../stretchr/objx/codegen/index.html | 86 - vendor/github.com/stretchr/objx/constants.go | 13 - .../github.com/stretchr/objx/conversions.go | 117 - .../stretchr/objx/conversions_test.go | 94 - vendor/github.com/stretchr/objx/doc.go | 72 - .../github.com/stretchr/objx/fixture_test.go | 98 - vendor/github.com/stretchr/objx/map.go | 222 - .../github.com/stretchr/objx/map_for_test.go | 10 - vendor/github.com/stretchr/objx/map_test.go | 147 - vendor/github.com/stretchr/objx/mutations.go | 81 - .../stretchr/objx/mutations_test.go | 77 - vendor/github.com/stretchr/objx/security.go | 14 - .../github.com/stretchr/objx/security_test.go | 12 - .../stretchr/objx/simple_example_test.go | 41 - vendor/github.com/stretchr/objx/tests.go | 17 - vendor/github.com/stretchr/objx/tests_test.go | 24 - .../stretchr/objx/type_specific_codegen.go | 2881 -- .../objx/type_specific_codegen_test.go | 2867 -- vendor/github.com/stretchr/objx/value.go | 13 - vendor/github.com/stretchr/objx/value_test.go | 1 - .../stretchr/testify/assert/assertions.go | 844 - .../testify/assert/assertions_test.go | 788 - .../github.com/stretchr/testify/assert/doc.go | 154 - .../stretchr/testify/assert/errors.go | 10 - .../testify/assert/forward_assertions.go | 265 - .../testify/assert/forward_assertions_test.go | 511 - .../testify/assert/http_assertions.go | 157 - .../testify/assert/http_assertions_test.go | 86 - .../github.com/stretchr/testify/mock/doc.go | 43 - .../github.com/stretchr/testify/mock/mock.go | 559 - .../stretchr/testify/mock/mock_test.go | 794 - .../goblin => tobi/airbrake-go}/LICENSE | 2 +- vendor/github.com/tobi/airbrake-go/README | 28 + .../github.com/tobi/airbrake-go/airbrake.go | 263 + .../tobi/airbrake-go/airbrake_test.go | 125 + vendor/github.com/tobi/airbrake-go/handler.go | 17 + .../github.com/ungerik/go-gravatar/.gitignore | 24 - vendor/github.com/ungerik/go-gravatar/LICENSE | 9 - vendor/github.com/ungerik/go-gravatar/README | 12 - .../ungerik/go-gravatar/gravatar.go | 73 - .../vrischmann/envconfig/.travis.yml | 7 - .../github.com/vrischmann/envconfig/README.md | 81 - vendor/github.com/vrischmann/envconfig/doc.go | 128 - .../vrischmann/envconfig/envconfig.go | 350 - .../vrischmann/envconfig/envconfig_test.go | 361 - .../vrischmann/envconfig/example_test.go | 78 - .../github.com/vrischmann/envconfig/slice.go | 76 - .../vrischmann/envconfig/slice_test.go | 76 - vendor/golang.org/x/crypto/md4/md4.go | 118 + vendor/golang.org/x/crypto/md4/md4_test.go | 71 + vendor/golang.org/x/crypto/md4/md4block.go | 89 + vendor/golang.org/x/net/context/context.go | 2 +- .../golang.org/x/net/context/context_test.go | 2 +- .../x/net/context/ctxhttp/cancelreq.go | 18 + .../x/net/context/ctxhttp/cancelreq_go14.go | 23 + .../x/net/context/ctxhttp/ctxhttp.go | 79 + .../x/net/context/ctxhttp/ctxhttp_test.go | 72 + .../x/net/context/withtimeout_test.go | 2 +- .../validator.v5}/LICENSE | 0 .../bluesuncorp/validator.v5/README.md | 154 + .../validator.v5}/baked_in.go | 371 +- .../validator.v5/benchmarks_test.go | 163 + .../validator.v5}/doc.go | 231 +- .../validator.v5/examples/simple.go | 85 + .../bluesuncorp/validator.v5/examples_test.go | 95 + .../bluesuncorp/validator.v5/regexes.go | 64 + .../bluesuncorp/validator.v5/validator.go | 1031 + .../validator.v5/validator_test.go | 3927 +++ vendor/gopkg.in/gorp.v1/LICENSE | 22 + vendor/gopkg.in/gorp.v1/Makefile | 6 + vendor/gopkg.in/gorp.v1/README.md | 672 + vendor/gopkg.in/gorp.v1/dialect.go | 692 + vendor/gopkg.in/gorp.v1/errors.go | 26 + vendor/gopkg.in/gorp.v1/gorp.go | 2085 ++ vendor/gopkg.in/gorp.v1/gorp_test.go | 2083 ++ vendor/gopkg.in/gorp.v1/test_all.sh | 22 + .../go-validate-yourself.v4/.gitignore | 24 - .../go-validate-yourself.v4/.travis.yml | 16 - .../go-validate-yourself.v4/README.md | 43 - .../go-validate-yourself.v4/regexes.go | 36 - .../go-validate-yourself.v4/validator.go | 375 - .../go-validate-yourself.v4/validator_test.go | 1809 -- vendor/gopkg.in/yaml.v1/LICENSE | 185 + vendor/gopkg.in/yaml.v1/LICENSE.libyaml | 19 + vendor/gopkg.in/yaml.v1/README.md | 128 + vendor/gopkg.in/yaml.v1/apic.go | 742 + vendor/gopkg.in/yaml.v1/decode.go | 538 + vendor/gopkg.in/yaml.v1/decode_test.go | 648 + vendor/gopkg.in/yaml.v1/emitterc.go | 1682 ++ vendor/gopkg.in/yaml.v1/encode.go | 226 + vendor/gopkg.in/yaml.v1/encode_test.go | 386 + vendor/gopkg.in/yaml.v1/parserc.go | 1096 + vendor/gopkg.in/yaml.v1/readerc.go | 391 + vendor/gopkg.in/yaml.v1/resolve.go | 148 + vendor/gopkg.in/yaml.v1/scannerc.go | 2710 ++ vendor/gopkg.in/yaml.v1/sorter.go | 104 + vendor/gopkg.in/yaml.v1/suite_test.go | 12 + vendor/gopkg.in/yaml.v1/writerc.go | 89 + vendor/gopkg.in/yaml.v1/yaml.go | 306 + vendor/gopkg.in/yaml.v1/yamlh.go | 712 + vendor/gopkg.in/yaml.v1/yamlprivateh.go | 173 + vendor/gopkg.in/yaml.v2/decode_test.go | 6 +- vendor/gopkg.in/yaml.v2/encode_test.go | 11 +- vendor/gopkg.in/yaml.v2/yaml.go | 2 +- yaml/matrix/matrix.go | 2 +- yaml/matrix/matrix_test.go | 32 +- yaml/parse.go | 15 - yaml/secure/secure.go | 46 - yaml/yaml.go | 16 + 719 files changed, 128547 insertions(+), 34572 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile.alpine delete mode 100644 contrib/debian/README create mode 100644 contrib/generate-amber.go create mode 100644 contrib/generate-js.go create mode 100644 contrib/setup-sqlite.sh delete mode 100644 controller/badge_test.go create mode 100644 controller/build.go delete mode 100644 controller/commits.go rename controller/{hooks.go => hook.go} (51%) create mode 100644 controller/index.go create mode 100644 controller/node.go delete mode 100644 controller/queue.go delete mode 100644 controller/recorder/recorder.go create mode 100644 controller/repo.go delete mode 100644 controller/repos.go delete mode 100644 controller/server.go create mode 100644 controller/stream.go delete mode 100644 controller/user_test.go delete mode 100644 controller/ws.go create mode 100644 drone.go create mode 100644 engine/engine.go create mode 100644 engine/pool.go create mode 100644 engine/pool_test.go delete mode 100644 engine/queue.go delete mode 100644 engine/queue_test.go delete mode 100644 engine/runner.go create mode 100644 engine/types.go create mode 100644 engine/util.go delete mode 100644 make.go create mode 100644 model/build_test.go rename shared/ccmenu/ccmenu.go => model/cc.go (70%) create mode 100644 model/cc_test.go delete mode 100644 model/config.go create mode 100644 model/const.go create mode 100644 model/feed.go delete mode 100644 model/hook.go create mode 100644 model/job_test.go create mode 100644 model/key.go create mode 100644 model/key_test.go create mode 100644 model/log.go create mode 100644 model/log_test.go create mode 100644 model/netrc.go create mode 100644 model/node.go create mode 100644 model/node_test.go create mode 100644 model/perm.go create mode 100644 model/repo_test.go create mode 100644 model/star.go create mode 100644 model/star_test.go delete mode 100644 model/status.go create mode 100644 model/sys.go delete mode 100644 model/system.go create mode 100644 model/user_test.go delete mode 100644 model/util.go delete mode 100644 model/util_test.go create mode 100644 remote/github/github.go delete mode 100644 remote/github/github/github.go rename remote/github/{github => }/helper.go (97%) create mode 100644 remote/github/types.go rename remote/gitlab/{gitlab => }/gitlab.go (53%) rename remote/gitlab/{gitlab => }/gitlab_test.go (62%) rename remote/gitlab/{gitlab => }/helper.go (96%) rename remote/gitlab/{gitlab => }/testdata/hooks.go (100%) rename remote/gitlab/{gitlab => }/testdata/oauth.go (100%) rename remote/gitlab/{gitlab => }/testdata/projects.go (100%) rename remote/gitlab/{gitlab => }/testdata/testdata.go (100%) rename remote/gitlab/{gitlab => }/testdata/users.go (100%) create mode 100644 router/middleware/context/context.go create mode 100644 router/middleware/header/header.go create mode 100644 router/middleware/session/repo.go create mode 100644 router/middleware/session/user.go create mode 100644 router/router.go create mode 100644 shared/crypto/crypto.go rename yaml/secure/secure_test.go => shared/crypto/crypto_test.go (86%) delete mode 100644 shared/crypto/sshutil/sshutil.go delete mode 100644 shared/crypto/sshutil/sshutil_test.go create mode 100644 shared/database/database.go create mode 100644 shared/database/mysql/1_init.sql create mode 100644 shared/database/postgres/1_init.sql create mode 100644 shared/database/rebind.go create mode 100644 shared/database/sqlite3/1_init.sql create mode 100644 shared/docker/docker.go create mode 100644 shared/envconfig/envconfig.go create mode 100644 shared/oauth2/oauth2.go create mode 100644 shared/server/server.go delete mode 100644 shared/token/token_test.go create mode 100644 static/images/docker.svg create mode 100644 static/images/docker_blue.svg create mode 100644 static/images/docker_white.svg create mode 100644 static/images/favicon.ico create mode 100644 static/images/favicon.png create mode 100644 static/images/logo_dark.svg create mode 100644 static/images/logo_light.svg create mode 100644 static/images/ubuntu.svg create mode 100644 static/scripts/build.js create mode 100644 static/scripts/csrf.js create mode 100644 static/scripts/models.js create mode 100644 static/scripts/nodes.js create mode 100644 static/scripts/repo.js create mode 100644 static/scripts/repos.js create mode 100644 static/scripts/search.js create mode 100644 static/scripts/term.js create mode 100644 static/scripts/term_buf.js create mode 100644 static/scripts/users.js create mode 100644 static/scripts/utils.js create mode 100644 static/scripts/vendor/livestamp.js create mode 100644 static/scripts/vendor/plates.js create mode 100644 static/scripts_gen/.gitkeep create mode 100644 static/static.go create mode 100644 static/styles/header.sass create mode 100644 static/styles/modules/badges.sass create mode 100644 static/styles/modules/navbar.sass create mode 100644 static/styles/modules/range.sass create mode 100644 static/styles/modules/status.sass create mode 100644 static/styles/modules/subnav.sass create mode 100644 static/styles/modules/switch.sass create mode 100644 static/styles/modules/timeline.sass create mode 100644 static/styles/pages/build.sass create mode 100644 static/styles/pages/login.sass create mode 100644 static/styles/pages/repo.sass create mode 100644 static/styles/pages/user.sass create mode 100644 static/styles/pages/users.sass create mode 100644 static/styles/search.sass create mode 100644 static/styles/style.sass create mode 100644 static/styles_gen/.gitkeep create mode 100644 template/amber/400.amber create mode 100644 template/amber/401.amber create mode 100644 template/amber/403.amber create mode 100644 template/amber/404.amber create mode 100644 template/amber/500.amber create mode 100644 template/amber/_feed.amber create mode 100644 template/amber/_users.amber create mode 100644 template/amber/base.amber create mode 100644 template/amber/build.amber create mode 100644 template/amber/login.amber create mode 100644 template/amber/nodes.amber create mode 100644 template/amber/repo.amber create mode 100644 template/amber/repo_activate.amber create mode 100644 template/amber/repo_badge.amber create mode 100644 template/amber/repo_config.amber create mode 100644 template/amber/repo_secret.amber create mode 100644 template/amber/repos.amber create mode 100644 template/amber/user.amber create mode 100644 template/amber/users.amber create mode 100644 template/amber_gen/.gitkeep create mode 100644 template/template.go create mode 100644 vendor/code.google.com/p/go.crypto/ssh/agent.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/agent/client.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/agent/client_test.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/agent/forward.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/agent/keyring.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/agent/server.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/agent/server_test.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/agent/testdata_test.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/benchmark_test.go create mode 100644 vendor/code.google.com/p/go.crypto/ssh/common_test.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/connection.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/handshake.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/handshake_test.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/mux.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/mux_test.go create mode 100644 vendor/code.google.com/p/go.crypto/ssh/server_terminal.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/terminal/util_windows.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/test/agent_unix_test.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/test/cert_test.go create mode 100644 vendor/code.google.com/p/go.crypto/ssh/test/keys_test.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/test/testdata_test.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/testdata/doc.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/testdata/keys.go delete mode 100644 vendor/code.google.com/p/go.crypto/ssh/testdata_test.go delete mode 100644 vendor/github.com/Bugagazavr/go-gitlab-client/.gitignore delete mode 100644 vendor/github.com/Bugagazavr/go-gitlab-client/.travis.yml delete mode 100644 vendor/github.com/BurntSushi/migration/Makefile delete mode 100644 vendor/github.com/BurntSushi/migration/README.md delete mode 100644 vendor/github.com/BurntSushi/migration/UNLICENSE delete mode 100644 vendor/github.com/BurntSushi/migration/doc.go delete mode 100644 vendor/github.com/BurntSushi/migration/migration.go delete mode 100644 vendor/github.com/BurntSushi/migration/session.vim delete mode 100644 vendor/github.com/Sirupsen/logrus/.gitignore delete mode 100644 vendor/github.com/Sirupsen/logrus/.travis.yml delete mode 100644 vendor/github.com/Sirupsen/logrus/hooks/airbrake/airbrake_test.go delete mode 100644 vendor/github.com/Sirupsen/logrus/json_formatter_test.go create mode 100644 vendor/github.com/codegangsta/cli/LICENSE create mode 100644 vendor/github.com/codegangsta/cli/README.md create mode 100644 vendor/github.com/codegangsta/cli/app.go create mode 100644 vendor/github.com/codegangsta/cli/app_test.go create mode 100644 vendor/github.com/codegangsta/cli/autocomplete/bash_autocomplete create mode 100644 vendor/github.com/codegangsta/cli/autocomplete/zsh_autocomplete create mode 100644 vendor/github.com/codegangsta/cli/cli.go create mode 100644 vendor/github.com/codegangsta/cli/cli_test.go create mode 100644 vendor/github.com/codegangsta/cli/command.go create mode 100644 vendor/github.com/codegangsta/cli/command_test.go create mode 100644 vendor/github.com/codegangsta/cli/context.go create mode 100644 vendor/github.com/codegangsta/cli/context_test.go create mode 100644 vendor/github.com/codegangsta/cli/flag.go create mode 100644 vendor/github.com/codegangsta/cli/flag_test.go create mode 100644 vendor/github.com/codegangsta/cli/help.go create mode 100644 vendor/github.com/codegangsta/cli/helpers_test.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/README.md create mode 100644 vendor/github.com/denisenkom/go-mssqldb/buf.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/charset.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/collation.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp1250.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp1251.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp1252.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp1253.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp1254.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp1255.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp1256.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp1257.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp1258.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp437.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp850.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp874.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp932.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp936.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp949.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/cp950.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/decimal.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/decimal_test.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/error.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/examples/simple.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/examples/tsql.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/log.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/mssql.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/mssql_go1.3.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/mssql_go1.3pre.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/net.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/ntlm.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/ntlm_test.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/parser.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/parser_test.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/queries_test.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/rpc.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/sspi_windows.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/tds.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/tds_test.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/token.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/tran.go create mode 100644 vendor/github.com/denisenkom/go-mssqldb/types.go delete mode 100644 vendor/github.com/dgrijalva/jwt-go/.gitignore delete mode 100644 vendor/github.com/dgrijalva/jwt-go/.travis.yml rename shared/docker/copy.go => vendor/github.com/docker/docker/pkg/stdcopy/stdcopy.go (65%) create mode 100644 vendor/github.com/docker/docker/pkg/stdcopy/stdcopy_test.go create mode 100644 vendor/github.com/docker/docker/pkg/units/duration.go create mode 100644 vendor/github.com/docker/docker/pkg/units/duration_test.go create mode 100644 vendor/github.com/docker/docker/pkg/units/size.go create mode 100644 vendor/github.com/docker/docker/pkg/units/size_test.go rename vendor/github.com/{vrischmann/envconfig => dustin/go-broadcast}/LICENSE (96%) create mode 100644 vendor/github.com/dustin/go-broadcast/README.markdown create mode 100644 vendor/github.com/dustin/go-broadcast/broadcaster.go create mode 100644 vendor/github.com/dustin/go-broadcast/broadcaster_test.go create mode 100644 vendor/github.com/dustin/go-broadcast/mux_observer.go create mode 100644 vendor/github.com/dustin/go-broadcast/mux_observer_test.go create mode 100644 vendor/github.com/eknkc/amber/README.md create mode 100644 vendor/github.com/eknkc/amber/amber_test.go create mode 100644 vendor/github.com/eknkc/amber/amberc/cli.go create mode 100644 vendor/github.com/eknkc/amber/compiler.go create mode 100644 vendor/github.com/eknkc/amber/doc.go create mode 100644 vendor/github.com/eknkc/amber/parser/nodes.go create mode 100644 vendor/github.com/eknkc/amber/parser/parser.go create mode 100644 vendor/github.com/eknkc/amber/parser/scanner.go create mode 100644 vendor/github.com/eknkc/amber/runtime.go create mode 100644 vendor/github.com/eknkc/amber/samples/basic.amber create mode 100644 vendor/github.com/eknkc/amber/samples/compiledir_test/basic.amber create mode 100644 vendor/github.com/eknkc/amber/samples/compiledir_test/compiledir_test/basic.amber create mode 100644 vendor/github.com/eknkc/amber/samples/inherit.amber create mode 100644 vendor/github.com/eknkc/amber/samples/inherit.master.amber create mode 100644 vendor/github.com/eknkc/amber/samples/multilevel.inheritance.a.amber create mode 100644 vendor/github.com/eknkc/amber/samples/multilevel.inheritance.b.amber create mode 100644 vendor/github.com/eknkc/amber/samples/multilevel.inheritance.c.amber delete mode 100644 vendor/github.com/franela/goblin/.gitignore delete mode 100644 vendor/github.com/franela/goblin/.travis.yml delete mode 100644 vendor/github.com/franela/goblin/Makefile delete mode 100644 vendor/github.com/franela/goblin/README.md delete mode 100644 vendor/github.com/franela/goblin/assertions.go delete mode 100644 vendor/github.com/franela/goblin/assertions_test.go delete mode 100644 vendor/github.com/franela/goblin/describe_test.go delete mode 100644 vendor/github.com/franela/goblin/goblin.go delete mode 100644 vendor/github.com/franela/goblin/goblin_logo.jpg delete mode 100644 vendor/github.com/franela/goblin/goblin_output.png delete mode 100644 vendor/github.com/franela/goblin/goblin_test.go delete mode 100644 vendor/github.com/franela/goblin/it_test.go delete mode 100644 vendor/github.com/franela/goblin/mono_reporter.go delete mode 100644 vendor/github.com/franela/goblin/reporting.go delete mode 100644 vendor/github.com/franela/goblin/reporting_test.go delete mode 100644 vendor/github.com/franela/goblin/resolver.go delete mode 100644 vendor/github.com/franela/goblin/resolver_test.go create mode 100644 vendor/github.com/getsentry/raven-go/Dockerfile.test rename vendor/github.com/{namsral/flag => getsentry/raven-go}/LICENSE (87%) create mode 100644 vendor/github.com/getsentry/raven-go/README.md create mode 100644 vendor/github.com/getsentry/raven-go/client.go create mode 100644 vendor/github.com/getsentry/raven-go/client_test.go create mode 100644 vendor/github.com/getsentry/raven-go/docs/Makefile create mode 100644 vendor/github.com/getsentry/raven-go/docs/conf.py create mode 100644 vendor/github.com/getsentry/raven-go/docs/index.rst create mode 100644 vendor/github.com/getsentry/raven-go/docs/make.bat create mode 100644 vendor/github.com/getsentry/raven-go/docs/sentry-doc-config.json create mode 100644 vendor/github.com/getsentry/raven-go/example/example.go create mode 100644 vendor/github.com/getsentry/raven-go/examples_test.go create mode 100644 vendor/github.com/getsentry/raven-go/exception.go create mode 100644 vendor/github.com/getsentry/raven-go/exception_test.go create mode 100644 vendor/github.com/getsentry/raven-go/http.go create mode 100644 vendor/github.com/getsentry/raven-go/http_test.go create mode 100644 vendor/github.com/getsentry/raven-go/interfaces.go create mode 100644 vendor/github.com/getsentry/raven-go/runtests.sh create mode 100644 vendor/github.com/getsentry/raven-go/stacktrace.go create mode 100644 vendor/github.com/getsentry/raven-go/stacktrace_test.go create mode 100644 vendor/github.com/getsentry/raven-go/writer.go delete mode 100644 vendor/github.com/gin-gonic/gin/.gitignore delete mode 100644 vendor/github.com/gin-gonic/gin/.travis.yml create mode 100644 vendor/github.com/gin-gonic/gin/BENCHMARKS.md create mode 100644 vendor/github.com/gin-gonic/gin/benchmarks_test.go create mode 100644 vendor/github.com/gin-gonic/gin/binding/default_validator.go create mode 100644 vendor/github.com/gin-gonic/gin/errors_test.go create mode 100644 vendor/github.com/gin-gonic/gin/examples/app-engine/README.md create mode 100644 vendor/github.com/gin-gonic/gin/examples/app-engine/app.yaml create mode 100644 vendor/github.com/gin-gonic/gin/examples/app-engine/hello.go create mode 100644 vendor/github.com/gin-gonic/gin/examples/basic/main.go create mode 100644 vendor/github.com/gin-gonic/gin/examples/realtime-advanced/main.go create mode 100644 vendor/github.com/gin-gonic/gin/examples/realtime-advanced/resources/static/realtime.js create mode 100644 vendor/github.com/gin-gonic/gin/examples/realtime-advanced/rooms.go create mode 100644 vendor/github.com/gin-gonic/gin/examples/realtime-advanced/routes.go create mode 100644 vendor/github.com/gin-gonic/gin/examples/realtime-advanced/stats.go create mode 100644 vendor/github.com/gin-gonic/gin/examples/realtime-chat/main.go create mode 100644 vendor/github.com/gin-gonic/gin/examples/realtime-chat/rooms.go create mode 100644 vendor/github.com/gin-gonic/gin/examples/realtime-chat/template.go create mode 100644 vendor/github.com/gin-gonic/gin/fs.go create mode 100644 vendor/github.com/gin-gonic/gin/gin_integration_test.go delete mode 100644 vendor/github.com/gin-gonic/gin/render/file.go delete mode 100644 vendor/github.com/go-sql-driver/mysql/.gitignore delete mode 100644 vendor/github.com/go-sql-driver/mysql/.travis.yml delete mode 100644 vendor/github.com/gorilla/securecookie/.travis.yml delete mode 100644 vendor/github.com/hashicorp/golang-lru/.gitignore delete mode 100644 vendor/github.com/lib/pq/.gitignore delete mode 100644 vendor/github.com/lib/pq/.travis.yml delete mode 100644 vendor/github.com/lib/pq/certs/README delete mode 100644 vendor/github.com/lib/pq/certs/postgresql.crt delete mode 100644 vendor/github.com/lib/pq/certs/postgresql.key delete mode 100644 vendor/github.com/lib/pq/certs/root.crt delete mode 100644 vendor/github.com/lib/pq/certs/server.crt delete mode 100644 vendor/github.com/lib/pq/certs/server.key create mode 100644 vendor/github.com/lib/pq/conn_xact_test.go delete mode 100644 vendor/github.com/lib/pq/ssl_test.go delete mode 100644 vendor/github.com/manucorporat/sse/.travis.yml rename vendor/github.com/{stretchr/objx/LICENSE.md => manucorporat/sse/LICENSE} (85%) create mode 100644 vendor/github.com/manucorporat/sse/sse-decoder.go create mode 100644 vendor/github.com/manucorporat/sse/sse-decoder_test.go create mode 100644 vendor/github.com/manucorporat/sse/writer.go create mode 100644 vendor/github.com/manucorporat/stats/stats.go delete mode 100644 vendor/github.com/mattn/go-sqlite3/.gitignore delete mode 100644 vendor/github.com/mattn/go-sqlite3/.travis.yml rename vendor/github.com/mattn/go-sqlite3/{sqlite3-binding.c => sqlite3.c} (100%) rename vendor/github.com/mattn/go-sqlite3/{sqlite3-binding.h => sqlite3.h} (100%) create mode 100644 vendor/github.com/mitchellh/cli/LICENSE create mode 100644 vendor/github.com/mitchellh/cli/README.md create mode 100644 vendor/github.com/mitchellh/cli/cli.go create mode 100644 vendor/github.com/mitchellh/cli/cli_test.go create mode 100644 vendor/github.com/mitchellh/cli/command.go create mode 100644 vendor/github.com/mitchellh/cli/command_mock.go create mode 100644 vendor/github.com/mitchellh/cli/command_mock_test.go create mode 100644 vendor/github.com/mitchellh/cli/help.go create mode 100644 vendor/github.com/mitchellh/cli/ui.go create mode 100644 vendor/github.com/mitchellh/cli/ui_colored.go create mode 100644 vendor/github.com/mitchellh/cli/ui_colored_test.go create mode 100644 vendor/github.com/mitchellh/cli/ui_concurrent.go create mode 100644 vendor/github.com/mitchellh/cli/ui_concurrent_test.go create mode 100644 vendor/github.com/mitchellh/cli/ui_mock.go create mode 100644 vendor/github.com/mitchellh/cli/ui_mock_test.go create mode 100644 vendor/github.com/mitchellh/cli/ui_test.go create mode 100644 vendor/github.com/mitchellh/cli/ui_writer.go create mode 100644 vendor/github.com/mitchellh/cli/ui_writer_test.go delete mode 100644 vendor/github.com/namsral/flag/README.md delete mode 100644 vendor/github.com/namsral/flag/example_test.go delete mode 100644 vendor/github.com/namsral/flag/examples/gopher.conf delete mode 100644 vendor/github.com/namsral/flag/examples/gopher.go delete mode 100644 vendor/github.com/namsral/flag/export_test.go delete mode 100644 vendor/github.com/namsral/flag/flag.go delete mode 100644 vendor/github.com/namsral/flag/flag_test.go delete mode 100644 vendor/github.com/namsral/flag/testdata/test.conf create mode 100644 vendor/github.com/olekukonko/tablewriter/LICENCE.md create mode 100644 vendor/github.com/olekukonko/tablewriter/README.md create mode 100644 vendor/github.com/olekukonko/tablewriter/csv.go create mode 100644 vendor/github.com/olekukonko/tablewriter/csv2table/README.md create mode 100644 vendor/github.com/olekukonko/tablewriter/csv2table/csv2table.go create mode 100644 vendor/github.com/olekukonko/tablewriter/table.go create mode 100644 vendor/github.com/olekukonko/tablewriter/table_test.go create mode 100644 vendor/github.com/olekukonko/tablewriter/test.csv create mode 100644 vendor/github.com/olekukonko/tablewriter/test_info.csv create mode 100644 vendor/github.com/olekukonko/tablewriter/util.go create mode 100644 vendor/github.com/olekukonko/tablewriter/wrap.go create mode 100644 vendor/github.com/olekukonko/tablewriter/wrap_test.go create mode 100644 vendor/github.com/rubenv/sql-migrate/README.md create mode 100644 vendor/github.com/rubenv/sql-migrate/bindata_test.go create mode 100644 vendor/github.com/rubenv/sql-migrate/doc.go create mode 100644 vendor/github.com/rubenv/sql-migrate/init_test.go create mode 100644 vendor/github.com/rubenv/sql-migrate/migrate.go create mode 100644 vendor/github.com/rubenv/sql-migrate/migrate_test.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sort_test.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sql-migrate/command_common.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sql-migrate/command_down.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sql-migrate/command_redo.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sql-migrate/command_status.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sql-migrate/command_up.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sql-migrate/config.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sql-migrate/main.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sql-migrate/main_test.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sql-migrate/mssql.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sqlparse/README.md create mode 100644 vendor/github.com/rubenv/sql-migrate/sqlparse/sqlparse.go create mode 100644 vendor/github.com/rubenv/sql-migrate/sqlparse/sqlparse_test.go create mode 100644 vendor/github.com/rubenv/sql-migrate/test-integration/dbconfig.yml create mode 100644 vendor/github.com/rubenv/sql-migrate/test-integration/mysql-flag.sh create mode 100644 vendor/github.com/rubenv/sql-migrate/test-integration/mysql.sh create mode 100644 vendor/github.com/rubenv/sql-migrate/test-integration/postgres.sh create mode 100644 vendor/github.com/rubenv/sql-migrate/test-integration/sqlite.sh create mode 100644 vendor/github.com/rubenv/sql-migrate/test-migrations/1_initial.sql create mode 100644 vendor/github.com/rubenv/sql-migrate/test-migrations/2_record.sql create mode 100644 vendor/github.com/rubenv/sql-migrate/toapply_test.go delete mode 100644 vendor/github.com/russross/meddler/.gitignore delete mode 100644 vendor/github.com/samalba/dockerclient/.gitignore create mode 100644 vendor/github.com/samalba/dockerclient/examples/stats/stats.go delete mode 100644 vendor/github.com/square/go-jose/.gitignore delete mode 100644 vendor/github.com/square/go-jose/.travis.yml delete mode 100644 vendor/github.com/stretchr/objx/.gitignore delete mode 100644 vendor/github.com/stretchr/objx/README.md delete mode 100644 vendor/github.com/stretchr/objx/accessors.go delete mode 100644 vendor/github.com/stretchr/objx/accessors_test.go delete mode 100644 vendor/github.com/stretchr/objx/codegen/index.html delete mode 100644 vendor/github.com/stretchr/objx/constants.go delete mode 100644 vendor/github.com/stretchr/objx/conversions.go delete mode 100644 vendor/github.com/stretchr/objx/conversions_test.go delete mode 100644 vendor/github.com/stretchr/objx/doc.go delete mode 100644 vendor/github.com/stretchr/objx/fixture_test.go delete mode 100644 vendor/github.com/stretchr/objx/map.go delete mode 100644 vendor/github.com/stretchr/objx/map_for_test.go delete mode 100644 vendor/github.com/stretchr/objx/map_test.go delete mode 100644 vendor/github.com/stretchr/objx/mutations.go delete mode 100644 vendor/github.com/stretchr/objx/mutations_test.go delete mode 100644 vendor/github.com/stretchr/objx/security.go delete mode 100644 vendor/github.com/stretchr/objx/security_test.go delete mode 100644 vendor/github.com/stretchr/objx/simple_example_test.go delete mode 100644 vendor/github.com/stretchr/objx/tests.go delete mode 100644 vendor/github.com/stretchr/objx/tests_test.go delete mode 100644 vendor/github.com/stretchr/objx/type_specific_codegen.go delete mode 100644 vendor/github.com/stretchr/objx/type_specific_codegen_test.go delete mode 100644 vendor/github.com/stretchr/objx/value.go delete mode 100644 vendor/github.com/stretchr/objx/value_test.go delete mode 100644 vendor/github.com/stretchr/testify/assert/assertions.go delete mode 100644 vendor/github.com/stretchr/testify/assert/assertions_test.go delete mode 100644 vendor/github.com/stretchr/testify/assert/doc.go delete mode 100644 vendor/github.com/stretchr/testify/assert/errors.go delete mode 100644 vendor/github.com/stretchr/testify/assert/forward_assertions.go delete mode 100644 vendor/github.com/stretchr/testify/assert/forward_assertions_test.go delete mode 100644 vendor/github.com/stretchr/testify/assert/http_assertions.go delete mode 100644 vendor/github.com/stretchr/testify/assert/http_assertions_test.go delete mode 100644 vendor/github.com/stretchr/testify/mock/doc.go delete mode 100644 vendor/github.com/stretchr/testify/mock/mock.go delete mode 100644 vendor/github.com/stretchr/testify/mock/mock_test.go rename vendor/github.com/{franela/goblin => tobi/airbrake-go}/LICENSE (94%) create mode 100644 vendor/github.com/tobi/airbrake-go/README create mode 100644 vendor/github.com/tobi/airbrake-go/airbrake.go create mode 100644 vendor/github.com/tobi/airbrake-go/airbrake_test.go create mode 100644 vendor/github.com/tobi/airbrake-go/handler.go delete mode 100644 vendor/github.com/ungerik/go-gravatar/.gitignore delete mode 100644 vendor/github.com/ungerik/go-gravatar/LICENSE delete mode 100644 vendor/github.com/ungerik/go-gravatar/README delete mode 100644 vendor/github.com/ungerik/go-gravatar/gravatar.go delete mode 100644 vendor/github.com/vrischmann/envconfig/.travis.yml delete mode 100644 vendor/github.com/vrischmann/envconfig/README.md delete mode 100644 vendor/github.com/vrischmann/envconfig/doc.go delete mode 100644 vendor/github.com/vrischmann/envconfig/envconfig.go delete mode 100644 vendor/github.com/vrischmann/envconfig/envconfig_test.go delete mode 100644 vendor/github.com/vrischmann/envconfig/example_test.go delete mode 100644 vendor/github.com/vrischmann/envconfig/slice.go delete mode 100644 vendor/github.com/vrischmann/envconfig/slice_test.go create mode 100644 vendor/golang.org/x/crypto/md4/md4.go create mode 100644 vendor/golang.org/x/crypto/md4/md4_test.go create mode 100644 vendor/golang.org/x/crypto/md4/md4block.go create mode 100644 vendor/golang.org/x/net/context/ctxhttp/cancelreq.go create mode 100644 vendor/golang.org/x/net/context/ctxhttp/cancelreq_go14.go create mode 100644 vendor/golang.org/x/net/context/ctxhttp/ctxhttp.go create mode 100644 vendor/golang.org/x/net/context/ctxhttp/ctxhttp_test.go rename vendor/gopkg.in/{joeybloggs/go-validate-yourself.v4 => bluesuncorp/validator.v5}/LICENSE (100%) create mode 100644 vendor/gopkg.in/bluesuncorp/validator.v5/README.md rename vendor/gopkg.in/{joeybloggs/go-validate-yourself.v4 => bluesuncorp/validator.v5}/baked_in.go (63%) create mode 100644 vendor/gopkg.in/bluesuncorp/validator.v5/benchmarks_test.go rename vendor/gopkg.in/{joeybloggs/go-validate-yourself.v4 => bluesuncorp/validator.v5}/doc.go (52%) create mode 100644 vendor/gopkg.in/bluesuncorp/validator.v5/examples/simple.go create mode 100644 vendor/gopkg.in/bluesuncorp/validator.v5/examples_test.go create mode 100644 vendor/gopkg.in/bluesuncorp/validator.v5/regexes.go create mode 100644 vendor/gopkg.in/bluesuncorp/validator.v5/validator.go create mode 100644 vendor/gopkg.in/bluesuncorp/validator.v5/validator_test.go create mode 100644 vendor/gopkg.in/gorp.v1/LICENSE create mode 100644 vendor/gopkg.in/gorp.v1/Makefile create mode 100644 vendor/gopkg.in/gorp.v1/README.md create mode 100644 vendor/gopkg.in/gorp.v1/dialect.go create mode 100644 vendor/gopkg.in/gorp.v1/errors.go create mode 100644 vendor/gopkg.in/gorp.v1/gorp.go create mode 100644 vendor/gopkg.in/gorp.v1/gorp_test.go create mode 100644 vendor/gopkg.in/gorp.v1/test_all.sh delete mode 100644 vendor/gopkg.in/joeybloggs/go-validate-yourself.v4/.gitignore delete mode 100644 vendor/gopkg.in/joeybloggs/go-validate-yourself.v4/.travis.yml delete mode 100644 vendor/gopkg.in/joeybloggs/go-validate-yourself.v4/README.md delete mode 100644 vendor/gopkg.in/joeybloggs/go-validate-yourself.v4/regexes.go delete mode 100644 vendor/gopkg.in/joeybloggs/go-validate-yourself.v4/validator.go delete mode 100644 vendor/gopkg.in/joeybloggs/go-validate-yourself.v4/validator_test.go create mode 100644 vendor/gopkg.in/yaml.v1/LICENSE create mode 100644 vendor/gopkg.in/yaml.v1/LICENSE.libyaml create mode 100644 vendor/gopkg.in/yaml.v1/README.md create mode 100644 vendor/gopkg.in/yaml.v1/apic.go create mode 100644 vendor/gopkg.in/yaml.v1/decode.go create mode 100644 vendor/gopkg.in/yaml.v1/decode_test.go create mode 100644 vendor/gopkg.in/yaml.v1/emitterc.go create mode 100644 vendor/gopkg.in/yaml.v1/encode.go create mode 100644 vendor/gopkg.in/yaml.v1/encode_test.go create mode 100644 vendor/gopkg.in/yaml.v1/parserc.go create mode 100644 vendor/gopkg.in/yaml.v1/readerc.go create mode 100644 vendor/gopkg.in/yaml.v1/resolve.go create mode 100644 vendor/gopkg.in/yaml.v1/scannerc.go create mode 100644 vendor/gopkg.in/yaml.v1/sorter.go create mode 100644 vendor/gopkg.in/yaml.v1/suite_test.go create mode 100644 vendor/gopkg.in/yaml.v1/writerc.go create mode 100644 vendor/gopkg.in/yaml.v1/yaml.go create mode 100644 vendor/gopkg.in/yaml.v1/yamlh.go create mode 100644 vendor/gopkg.in/yaml.v1/yamlprivateh.go delete mode 100644 yaml/parse.go delete mode 100644 yaml/secure/secure.go create mode 100644 yaml/yaml.go diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index e1b68ff0c0..0000000000 --- a/.dockerignore +++ /dev/null @@ -1,13 +0,0 @@ -bin/ -cmd/drone-server/drone_bindata.go -dist/ -doc/ - -.git/ -.dockerignore -.drone.yml -.gitignore -drone.sqlite -Dockerfile -LICENSE -README.md diff --git a/.drone.yml b/.drone.yml index 50ac7fa5a2..0bb3a368b7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,14 +8,12 @@ env: - PATH=$PATH:$GOROOT/bin:$GOPATH/bin script: - - go run make.go deps - - go run make.go bindata - - go run make.go vet - - go run make.go fmt - - go run make.go build - - go run make.go test - - - make dist + - apt-get -y -qq update + - apt-get -y -qq install libsqlite3-dev + - make deps + - make + - make test + - make deb notify: email: @@ -29,34 +27,26 @@ publish: bucket: downloads.drone.io access_key: $$AWS_KEY secret_key: $$AWS_SECRET - source: dist/drone.deb + source: contrib/debian/drone.deb target: $DRONE_BRANCH/ when: owner: drone +--- + clone: path: github.com/drone/drone build: - image: golang:1.5.0 + image: golang:1.5 commands: - - export GOPATH=/drone - - export PATH=$PATH:$GOPATH/bin - - - go run make.go deps - - go run make.go bindata - - go run make.go vet - - go run make.go fmt - - go run make.go build - - go run make.go test - - - make dist - -compose: - database: - image: mysql:5.5 - environment: - - MYSQL_ALLOW_EMPTY_PASSWORD=yes - - MYSQL_DATABASE=test + - apt-get -y -qq update + - apt-get -y -qq install libsqlite3-dev + - make deps + - make gen + - make test + - make build + - make build_static + - make deb diff --git a/.gitignore b/.gitignore index 63905c71f2..b69cf26f3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,13 @@ -drone.sublime-project -drone.sublime-workspace -.vagrant - -*~ -~* +drone +drone_* *.sqlite -*.sqlite3 -*.deb -*.deb.* -*.rpm -*.out -*.prof -*.rice-box.go -*.db +*_gen.go +*.html +*.css *.txt -*.min.css +*.zip +*.gz +*.out *.min.js -*_bindata.go -*.toml - -# generate binaries -cmd/drone-agent/drone-agent -cmd/drone-build/drone-build -cmd/drone-agent/drone-server - -# generated binaries in ./bin -bin/drone -bin/drone-agent -bin/drone-build -bin/drone-server - -# generated binaries in dpkg -dist/drone/usr/local/bin/drone +*.deb +temp/ diff --git a/Dockerfile b/Dockerfile index a3ee54084a..5a1c0b7c10 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,20 @@ -FROM golang:1.4.2 +# Build the drone executable on a x64 Linux host: +# +# go build --ldflags '-extldflags "-static"' -o drone_static +# +# +# Alternate command for Go 1.4 and older: +# +# go build -a -tags netgo --ldflags '-extldflags "-static"' -o drone_static +# +# +# Build the docker image: +# +# docker build --rm=true -t drone/drone . -ENV DRONE_SERVER_PORT :80 -WORKDIR $GOPATH/src/github.com/drone/drone +FROM centurylink/ca-certs +EXPOSE 8080 -EXPOSE 80 +ADD drone_static /drone_static -ENTRYPOINT ["/usr/local/bin/drone"] -CMD ["-config", "/tmp/drone.toml"] - -RUN apt-get update \ - && apt-get install -y libsqlite3-dev \ - && git clone git://github.com/gin-gonic/gin.git $GOPATH/src/github.com/gin-gonic/gin \ - && go get -u github.com/jteeuwen/go-bindata/... - -RUN touch /tmp/drone.toml - -ADD . . -RUN make bindata deps \ - && make build \ - && mv bin/* /usr/local/bin/ \ - && rm -rf bin cmd/drone-server/drone_bindata.go +ENTRYPOINT ["/drone_static"] \ No newline at end of file diff --git a/Dockerfile.alpine b/Dockerfile.alpine deleted file mode 100644 index 4ab4ba6aa0..0000000000 --- a/Dockerfile.alpine +++ /dev/null @@ -1,26 +0,0 @@ -# Docker image for the Drone build runner -# -# docker build --file=Dockerfile.alpine --rm=true -t drone/drone-alpine . - -FROM alpine:3.2 - -EXPOSE 8080 - -ENV GOROOT=/usr/lib/go \ - GOPATH=/gopath \ - GOBIN=/gopath/bin \ - PATH=$PATH:$GOROOT/bin:$GOPATH/bin - -WORKDIR /gopath/src/github.com/drone/drone -ADD . /gopath/src/github.com/drone/drone - -RUN apk add -U go ca-certificates libc-dev gcc git sqlite-libs && \ - go get github.com/jteeuwen/go-bindata/... && \ - /gopath/bin/go-bindata -o="cmd/drone-server/drone_bindata.go" cmd/drone-server/static/... && \ - go run make.go build && \ - apk del git go gcc libc-dev && \ - mv bin/drone /bin/drone && \ - rm -rf /gopath && \ - rm -rf /var/cache/apk/* - -ENTRYPOINT ["/bin/drone"] diff --git a/Makefile b/Makefile index ff91a94143..056a0f4cd1 100644 --- a/Makefile +++ b/Makefile @@ -1,39 +1,35 @@ -.PHONY: dist +.PHONY: vendor -SHA := $(shell git rev-parse --short HEAD) -VERSION := 0.4.0-alpha +PACKAGES = $(shell go list ./... | grep -v /vendor/) -all: build +all: gen build + +deps: + go get golang.org/x/tools/cmd/cover + go get golang.org/x/tools/cmd/vet + go get -u github.com/kr/vexp + go get -u github.com/eknkc/amber/amberc + go get -u github.com/jteeuwen/go-bindata/... + go get -u github.com/elazarl/go-bindata-assetfs/... + +gen: + go generate $(go list ./... | grep -v /vendor/) build: - go run make.go bindata build - - -# Execute the database test suite against mysql 5.5 -# -# You can launch a mysql container locally for testing: -# docker run -rm -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -e MYSQL_DATABASE=test -p 3306:3306 mysql:5.5 -test_mysql: - mysql -P 3306 --protocol=tcp -u root -e 'create database if not exists test;' - TEST_DRIVER="mysql" TEST_DATASOURCE="root@tcp(127.0.0.1:3306)/test" go test -short github.com/drone/drone/pkg/store/builtin - mysql -P 3306 --protocol=tcp -u root -e 'drop database test;' - -run: - bin/drone --debug - -# installs the drone binaries into bin -install: - install -t /usr/local/bin bin/drone - install -t /usr/local/bin bin/drone-agent - -docker: - docker build --file=cmd/drone-build/Dockerfile.alpine --rm=true -t drone/drone-build . - -# creates a debian package for drone -# to install `sudo dpkg -i drone.deb` -dist: - mkdir -p dist/drone/usr/local/bin - mkdir -p dist/drone/var/lib/drone - mkdir -p dist/drone/var/cache/drone - cp bin/drone dist/drone/usr/local/bin - -dpkg-deb --build dist/drone + GO15VENDOREXPERIMENT=1 go build + +build_static: + GO15VENDOREXPERIMENT=1 go build --ldflags '-extldflags "-static"' -o drone_static + +test: + go test -cover $(PACKAGES) + +deb: + mkdir -p contrib/debian/drone/usr/local/bin + mkdir -p contrib/debian/drone/var/lib/drone + mkdir -p contrib/debian/drone/var/cache/drone + cp drone contrib/debian/drone/usr/local/bin + -dpkg-deb --build contrib/debian/drone + +vendor: + vexp diff --git a/contrib/debian/README b/contrib/debian/README deleted file mode 100644 index f85ea17dc7..0000000000 --- a/contrib/debian/README +++ /dev/null @@ -1 +0,0 @@ -This is where Drone packages go after running "make dist" in the root directory. \ No newline at end of file diff --git a/contrib/generate-amber.go b/contrib/generate-amber.go new file mode 100644 index 0000000000..8e61754695 --- /dev/null +++ b/contrib/generate-amber.go @@ -0,0 +1,6 @@ +// +build ignore + +// This program converts amber templates to standard +// Go template files. + +package main diff --git a/contrib/generate-js.go b/contrib/generate-js.go new file mode 100644 index 0000000000..7e77d32605 --- /dev/null +++ b/contrib/generate-js.go @@ -0,0 +1,60 @@ +// +build ignore + +// This program minifies JavaScript files +// $ go run generate-js.go -dir scripts/ -out scripts/drone.min.js + +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/dchest/jsmin" +) + +var ( + dir = flag.String("dir", "scripts/", "") + out = flag.String("o", "scripts/drone.min.js", "") +) + +func main() { + flag.Parse() + + var buf bytes.Buffer + + // walk the directory tree and write all + // javascript files to the buffer. + filepath.Walk(*dir, func(path string, info os.FileInfo, err error) error { + if filepath.Ext(path) != ".js" { + return nil + } + + f, err := os.Open(path) + if err != nil { + return nil + } + defer f.Close() + + // write the file name to the minified output + fmt.Fprintf(&buf, "// %s\n", path) + + // copy the file to the buffer + _, err = io.Copy(&buf, f) + return err + }) + + // minifies the javascript + data, err := jsmin.Minify(buf.Bytes()) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // write the minified output + ioutil.WriteFile(*out, data, 0700) +} diff --git a/contrib/setup-sqlite.sh b/contrib/setup-sqlite.sh new file mode 100644 index 0000000000..1a13c7af87 --- /dev/null +++ b/contrib/setup-sqlite.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +cd /tmp + +curl -O https://www.sqlite.org/2015/sqlite-autoconf-3081101.tar.gz +tar xzf sqlite-autoconf-3081101.tar.gz +cd sqlite-autoconf-3081101 +cd sqlite-3.6.421 +./configure -prefix=/scratch/usr/local +make +make install diff --git a/controller/badge.go b/controller/badge.go index dea1e769e3..6a4604b66a 100644 --- a/controller/badge.go +++ b/controller/badge.go @@ -1,32 +1,29 @@ -package server +package controller import ( - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" + "github.com/gin-gonic/gin" - "github.com/drone/drone/pkg/ccmenu" - common "github.com/drone/drone/pkg/types" + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" ) var ( - badgeSuccess = []byte(`buildbuildsuccesssuccess`) - badgeFailure = []byte(`buildbuildfailurefailure`) - badgeStarted = []byte(`buildbuildstartedstarted`) - badgeError = []byte(`buildbuilderrorerror`) - badgeNone = []byte(`buildbuildnonenone`) + badgeSuccess = `buildbuildsuccesssuccess` + badgeFailure = `buildbuildfailurefailure` + badgeStarted = `buildbuildstartedstarted` + badgeError = `buildbuilderrorerror` + badgeNone = `buildbuildnonenone` ) -// GetBadge accepts a request to retrieve the named -// repo and branhes latest build details from the datastore -// and return an SVG badges representing the build results. -// -// GET /api/badge/:owner/:name/status.svg -// func GetBadge(c *gin.Context) { - var repo = ToRepo(c) - var store = ToDatastore(c) - var branch = c.Request.FormValue("branch") - if len(branch) == 0 { - branch = repo.Branch + db := context.Database(c) + repo, err := model.GetRepoName(db, + c.Param("owner"), + c.Param("name"), + ) + if err != nil { + c.AbortWithStatus(404) + return } // an SVG response is always served, even when error, so @@ -36,44 +33,48 @@ func GetBadge(c *gin.Context) { // if no commit was found then display // the 'none' badge, instead of throwing // an error response - build, err := store.BuildLast(repo, branch) + branch := c.Query("branch") + if len(branch) == 0 { + branch = repo.Branch + } + + build, err := model.GetBuildLast(db, repo, branch) if err != nil { - c.Writer.Write(badgeNone) + c.String(404, badgeNone) return } switch build.Status { - case common.StateSuccess: - c.Writer.Write(badgeSuccess) - case common.StateFailure: - c.Writer.Write(badgeFailure) - case common.StateError, common.StateKilled: - c.Writer.Write(badgeError) - case common.StatePending, common.StateRunning: - c.Writer.Write(badgeStarted) + case model.StatusSuccess: + c.String(200, badgeSuccess) + case model.StatusFailure: + c.String(200, badgeFailure) + case model.StatusError, model.StatusKilled: + c.String(200, badgeError) + case model.StatusPending, model.StatusRunning: + c.String(200, badgeStarted) default: - c.Writer.Write(badgeNone) + c.String(404, badgeNone) } } -// GetCC accepts a request to retrieve the latest build -// status for the given repository from the datastore and -// in CCTray XML format. -// -// GET /api/badge/:host/:owner/:name/cc.xml -// -// TODO(bradrydzewski) this will not return in-progress builds, which it should func GetCC(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - list, err := store.BuildList(repo, 1, 0) - if err != nil || len(list) == 0 { + db := context.Database(c) + repo, err := model.GetRepoName(db, + c.Param("owner"), + c.Param("name"), + ) + if err != nil { c.AbortWithStatus(404) return } - cc := ccmenu.NewCC(repo, list[0]) + builds, err := model.GetBuildList(db, repo) + if err != nil || len(builds) == 0 { + c.AbortWithStatus(404) + return + } - c.Writer.Header().Set("Content-Type", "application/xml") + cc := model.NewCC(repo, builds[0], "") c.XML(200, cc) } diff --git a/controller/badge_test.go b/controller/badge_test.go deleted file mode 100644 index 5dc9c56f38..0000000000 --- a/controller/badge_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package server - -import ( - "database/sql" - "encoding/xml" - "net/http" - "net/url" - "testing" - - "github.com/drone/drone/pkg/ccmenu" - "github.com/drone/drone/pkg/server/recorder" - "github.com/drone/drone/pkg/store/mock" - common "github.com/drone/drone/pkg/types" - - . "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/stretchr/testify/mock" -) - -var badgeTests = []struct { - branch string - badge []byte - state string - activity string - status string - err error -}{ - {"", badgeSuccess, common.StateSuccess, "Sleeping", "Success", nil}, - {"master", badgeSuccess, common.StateSuccess, "Sleeping", "Success", nil}, - {"", badgeStarted, common.StateRunning, "Building", "Unknown", nil}, - {"", badgeError, common.StateError, "Sleeping", "Exception", nil}, - {"", badgeError, common.StateKilled, "Sleeping", "Exception", nil}, - {"", badgeFailure, common.StateFailure, "Sleeping", "Failure", nil}, - {"", badgeNone, "", "", "", sql.ErrNoRows}, -} - -func TestBadges(t *testing.T) { - store := new(mocks.Store) - url_, _ := url.Parse("http://localhost:8080") - - g := Goblin(t) - g.Describe("Badges", func() { - - g.It("should serve svg badges", func() { - for _, test := range badgeTests { - rw := recorder.New() - ctx := &gin.Context{Engine: gin.Default(), Writer: rw} - ctx.Request = &http.Request{ - Form: url.Values{}, - } - if len(test.branch) != 0 { - ctx.Request.Form.Set("branch", test.branch) - } - - repo := &common.Repo{FullName: "foo/bar"} - ctx.Set("datastore", store) - ctx.Set("repo", repo) - - commit := &common.Build{Status: test.state} - store.On("BuildLast", repo, test.branch).Return(commit, test.err).Once() - GetBadge(ctx) - - g.Assert(rw.Code).Equal(200) - g.Assert(rw.Body.Bytes()).Equal(test.badge) - g.Assert(rw.HeaderMap.Get("Content-Type")).Equal("image/svg+xml") - } - }) - - g.It("should serve ccmenu xml", func() { - - for _, test := range badgeTests { - rw := recorder.New() - ctx := &gin.Context{Engine: gin.Default(), Writer: rw} - ctx.Request = &http.Request{URL: url_} - - repo := &common.Repo{FullName: "foo/bar"} - ctx.Set("datastore", store) - ctx.Set("repo", repo) - - commits := []*common.Build{ - &common.Build{Status: test.state}, - } - store.On("BuildList", repo, mock.AnythingOfType("int"), mock.AnythingOfType("int")).Return(commits, test.err).Once() - GetCC(ctx) - - // in an error scenario (ie no build exists) we should - // return a 404 not found error. - if test.err != nil { - g.Assert(rw.Status()).Equal(404) - continue - } - - // else parse the CCMenu xml output and verify - // it matches the expected values. - cc := &ccmenu.CCProjects{} - xml.Unmarshal(rw.Body.Bytes(), cc) - g.Assert(rw.Code).Equal(200) - g.Assert(cc.Project.Activity).Equal(test.activity) - g.Assert(cc.Project.LastBuildStatus).Equal(test.status) - g.Assert(rw.HeaderMap.Get("Content-Type")).Equal("application/xml; charset=utf-8") - } - }) - }) -} diff --git a/controller/build.go b/controller/build.go new file mode 100644 index 0000000000..81fbb7d78d --- /dev/null +++ b/controller/build.go @@ -0,0 +1,199 @@ +package controller + +import ( + "io" + "net/http" + "os" + "strconv" + "strings" + + log "github.com/Sirupsen/logrus" + "github.com/drone/drone/engine" + "github.com/drone/drone/shared/httputil" + "github.com/gin-gonic/gin" + + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/router/middleware/session" +) + +func GetBuilds(c *gin.Context) { + repo := session.Repo(c) + db := context.Database(c) + builds, err := model.GetBuildList(db, repo) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + return + } + c.IndentedJSON(http.StatusOK, builds) +} + +func GetBuild(c *gin.Context) { + repo := session.Repo(c) + db := context.Database(c) + + num, err := strconv.Atoi(c.Param("number")) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + build, err := model.GetBuildNumber(db, repo, num) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + jobs, _ := model.GetJobList(db, build) + + out := struct { + *model.Build + Jobs []*model.Job `json:"jobs"` + }{build, jobs} + + c.IndentedJSON(http.StatusOK, &out) +} + +func GetBuildLogs(c *gin.Context) { + repo := session.Repo(c) + db := context.Database(c) + + // the user may specify to stream the full logs, + // or partial logs, capped at 2MB. + full, _ := strconv.ParseBool(c.Params.ByName("full")) + + // parse the build number and job sequence number from + // the repquest parameter. + num, _ := strconv.Atoi(c.Params.ByName("number")) + seq, _ := strconv.Atoi(c.Params.ByName("job")) + + build, err := model.GetBuildNumber(db, repo, num) + if err != nil { + c.AbortWithError(404, err) + return + } + + job, err := model.GetJobNumber(db, build, seq) + if err != nil { + c.AbortWithError(404, err) + return + } + + r, err := model.GetLog(db, job) + if err != nil { + c.AbortWithError(404, err) + return + } + + defer r.Close() + if full { + io.Copy(c.Writer, r) + } else { + io.Copy(c.Writer, io.LimitReader(r, 2000000)) + } +} + +func DeleteBuild(c *gin.Context) { + c.String(http.StatusOK, "DeleteBuild") +} + +func PostBuild(c *gin.Context) { + + remote := context.Remote(c) + repo := session.Repo(c) + db := context.Database(c) + + num, err := strconv.Atoi(c.Param("number")) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + user, err := model.GetUser(db, repo.UserID) + if err != nil { + log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) + c.AbortWithError(500, err) + return + } + + build, err := model.GetBuildNumber(db, repo, num) + if err != nil { + log.Errorf("failure to get build %d. %s", num, err) + c.AbortWithError(404, err) + return + } + + // fetch the .drone.yml file from the database + raw, sec, err := remote.Script(user, repo, build) + if err != nil { + log.Errorf("failure to get .drone.yml for %s. %s", repo.FullName, err) + c.AbortWithError(404, err) + return + } + + key, _ := model.GetKey(db, repo) + netrc, err := remote.Netrc(user, repo) + if err != nil { + log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) + c.AbortWithError(500, err) + return + } + + jobs, err := model.GetJobList(db, build) + if err != nil { + log.Errorf("failure to get build %d jobs. %s", build.Number, err) + c.AbortWithError(404, err) + return + } + + // must not restart a running build + if build.Status == model.StatusPending || build.Status == model.StatusRunning { + c.AbortWithStatus(409) + return + } + + tx, err := db.Begin() + if err != nil { + c.AbortWithStatus(500) + return + } + defer tx.Rollback() + + build.Status = model.StatusPending + build.Started = 0 + build.Finished = 0 + for _, job := range jobs { + job.Status = model.StatusPending + job.Started = 0 + job.Finished = 0 + job.ExitCode = 0 + model.UpdateJob(db, job) + } + + err = model.UpdateBuild(db, build) + if err != nil { + c.AbortWithStatus(500) + return + } + + tx.Commit() + + c.JSON(202, build) + + engine_ := context.Engine(c) + go engine_.Schedule(&engine.Task{ + User: user, + Repo: repo, + Build: build, + Jobs: jobs, + Keys: key, + Netrc: netrc, + Config: string(raw), + Secret: string(sec), + System: &model.System{ + Link: httputil.GetURL(c.Request), + Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), + Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), + }, + }) + +} diff --git a/controller/commits.go b/controller/commits.go deleted file mode 100644 index fb811fe725..0000000000 --- a/controller/commits.go +++ /dev/null @@ -1,285 +0,0 @@ -package server - -import ( - "fmt" - "io" - "os" - "strconv" - "strings" - "time" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/pkg/queue" - common "github.com/drone/drone/pkg/types" - "github.com/drone/drone/pkg/utils/httputil" -) - -// GetCommit accepts a request to retrieve a commit -// from the datastore for the given repository and -// commit sequence. -// -// GET /api/repos/:owner/:name/:number -// -func GetBuild(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - num, err := strconv.Atoi(c.Params.ByName("number")) - if err != nil { - c.Fail(400, err) - return - } - build, err := store.BuildNumber(repo, num) - if err != nil { - c.Fail(404, err) - return - } - build.Jobs, err = store.JobList(build) - if err != nil { - c.Fail(404, err) - } else { - c.JSON(200, build) - } -} - -// GetCommits accepts a request to retrieve a list -// of commits from the datastore for the given repository. -// -// GET /api/repos/:owner/:name/builds -// -func GetBuilds(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - builds, err := store.BuildList(repo, 20, 0) - if err != nil { - c.Fail(404, err) - } else { - c.JSON(200, builds) - } -} - -// GetLogs accepts a request to retrieve logs from the -// datastore for the given repository, build and task -// number. -// -// GET /api/repos/:owner/:name/logs/:number/:task -// -func GetLogs(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - full, _ := strconv.ParseBool(c.Params.ByName("full")) - build, _ := strconv.Atoi(c.Params.ByName("number")) - job, _ := strconv.Atoi(c.Params.ByName("task")) - - path := fmt.Sprintf("/logs/%s/%v/%v", repo.FullName, build, job) - r, err := store.GetBlobReader(path) - if err != nil { - c.Fail(404, err) - return - } - - defer r.Close() - if full { - io.Copy(c.Writer, r) - } else { - io.Copy(c.Writer, io.LimitReader(r, 2000000)) - } -} - -// // PostBuildStatus accepts a request to create a new build -// // status. The created user status is returned in JSON -// // format if successful. -// // -// // POST /api/repos/:owner/:name/status/:number -// // -// func PostBuildStatus(c *gin.Context) { -// store := ToDatastore(c) -// repo := ToRepo(c) -// num, err := strconv.Atoi(c.Params.ByName("number")) -// if err != nil { -// c.Fail(400, err) -// return -// } -// in := &common.Status{} -// if !c.BindWith(in, binding.JSON) { -// c.AbortWithStatus(400) -// return -// } -// if err := store.SetBuildStatus(repo.Name, num, in); err != nil { -// c.Fail(400, err) -// } else { -// c.JSON(201, in) -// } -// } - -// RunBuild accepts a request to restart an existing build. -// -// POST /api/builds/:owner/:name/builds/:number -// -func RunBuild(c *gin.Context) { - remote := ToRemote(c) - store := ToDatastore(c) - queue_ := ToQueue(c) - repo := ToRepo(c) - - num, err := strconv.Atoi(c.Params.ByName("number")) - if err != nil { - c.Fail(400, err) - return - } - build, err := store.BuildNumber(repo, num) - if err != nil { - c.Fail(404, err) - return - } - build.Jobs, err = store.JobList(build) - if err != nil { - c.Fail(404, err) - return - } - - user, err := store.User(repo.UserID) - if err != nil { - c.Fail(404, err) - return - } - - // must not restart a running build - if build.Status == common.StatePending || build.Status == common.StateRunning { - c.AbortWithStatus(409) - return - } - - build.Status = common.StatePending - build.Started = 0 - build.Finished = 0 - for _, job := range build.Jobs { - job.Status = common.StatePending - job.Started = 0 - job.Finished = 0 - job.ExitCode = 0 - } - - err = store.SetBuild(build) - if err != nil { - c.Fail(500, err) - return - } - - netrc, err := remote.Netrc(user, repo) - if err != nil { - c.Fail(500, err) - return - } - - // featch the .drone.yml file from the database - raw, sec, err := remote.Script(user, repo, build) - if err != nil { - c.Fail(404, err) - return - } - - // get the previous build so taht we can send - // on status change notifications - last, _ := store.BuildLast(repo, build.Commit.Branch) - - c.JSON(202, build) - - queue_.Publish(&queue.Work{ - User: user, - Repo: repo, - Build: build, - BuildPrev: last, - Keys: repo.Keys, - Netrc: netrc, - Config: raw, - Secret: sec, - System: &common.System{ - Link: httputil.GetURL(c.Request), - Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), - Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), - }, - }) -} - -// KillBuild accepts a request to kill a running build. -// -// DELETE /api/builds/:owner/:name/builds/:number -// -func KillBuild(c *gin.Context) { - runner := ToRunner(c) - queue := ToQueue(c) - store := ToDatastore(c) - repo := ToRepo(c) - num, err := strconv.Atoi(c.Params.ByName("number")) - if err != nil { - c.Fail(400, err) - return - } - build, err := store.BuildNumber(repo, num) - if err != nil { - c.Fail(404, err) - return - } - build.Jobs, err = store.JobList(build) - if err != nil { - c.Fail(404, err) - return - } - - // must not restart a running build - if build.Status != common.StatePending && build.Status != common.StateRunning { - c.Fail(409, err) - return - } - - // remove from the queue if exists - // - // TODO(bradrydzewski) this could yield a race condition - // because other threads may also be accessing these items. - for _, item := range queue.Items() { - if item.Repo.FullName == repo.FullName && item.Build.Number == build.Number { - queue.Remove(item) - break - } - } - - build.Status = common.StateKilled - build.Finished = time.Now().Unix() - if build.Started == 0 { - build.Started = build.Finished - } - for _, job := range build.Jobs { - if job.Status != common.StatePending && job.Status != common.StateRunning { - continue - } - job.Status = common.StateKilled - job.Started = build.Started - job.Finished = build.Finished - } - err = store.SetBuild(build) - if err != nil { - c.Fail(500, err) - return - } - - for _, job := range build.Jobs { - runner.Cancel(job) - } - // // get the agent from the repository so we can - // // notify the agent to kill the build. - // agent, err := store.BuildAgent(repo.FullName, build.Number) - // if err != nil { - // c.JSON(200, build) - // return - // } - // url_, _ := url.Parse("http://" + agent.Addr) - // url_.Path = fmt.Sprintf("/cancel/%s/%v", repo.FullName, build.Number) - // resp, err := http.Post(url_.String(), "application/json", nil) - // if err != nil { - // c.Fail(500, err) - // return - // } - // defer resp.Body.Close() - - c.JSON(200, build) -} diff --git a/controller/gitlab.go b/controller/gitlab.go index 66d9f29fad..b606bcafdc 100644 --- a/controller/gitlab.go +++ b/controller/gitlab.go @@ -1,158 +1,105 @@ -package server +package controller import ( "fmt" - "strconv" + "net/http" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" + "github.com/gin-gonic/gin" - "github.com/drone/drone/pkg/token" + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/router/middleware/session" + "github.com/drone/drone/shared/token" ) -// RedirectSha accepts a request to retvie a redirect -// to job from the datastore for the given repository -// and commit sha -// -// GET /gitlab/:owner/:name/redirect/commits/:sha -// -// REASON: It required by GitLab, becuase we get only -// sha and ref name, but drone uses build numbers -func RedirectSha(c *gin.Context) { - var branch string - - store := ToDatastore(c) - repo := ToRepo(c) - sha := c.Params.ByName("sha") - - branch = c.Request.FormValue("branch") - if branch == "" { - branch = repo.Branch - } +func GetCommit(c *gin.Context) { + db := context.Database(c) + repo := session.Repo(c) - build, err := store.BuildSha(repo, sha, branch) + parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { + return repo.Hash, nil + }) if err != nil { - c.Redirect(301, "/") + c.AbortWithError(http.StatusBadRequest, err) return } - - c.Redirect(301, fmt.Sprintf("/%s/%s/%d", repo.Owner, repo.Name, build.Number)) - return -} - -// RedirectPullRequest accepts a request to retvie a redirect -// to job from the datastore for the given repository -// and pull request number -// -// GET /gitlab/:owner/:name/redirect/pulls/:number -// -// REASON: It required by GitLab, because we get only -// internal merge request id/ref/sha, but drone uses -// build numbers -func RedirectPullRequest(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - num, err := strconv.Atoi(c.Params.ByName("number")) - if err != nil { - c.Redirect(301, "/") + if parsed.Text != repo.FullName { + c.AbortWithStatus(http.StatusUnauthorized) return } - build, err := store.BuildPullRequestNumber(repo, num) + commit := c.Param("sha") + branch := c.Query("branch") + if len(branch) == 0 { + branch = repo.Branch + } + + build, err := model.GetBuildCommit(db, repo, commit, branch) if err != nil { - c.Redirect(301, "/") + c.AbortWithError(http.StatusNotFound, err) return } - c.Redirect(301, fmt.Sprintf("/%s/%s/%d", repo.Owner, repo.Name, build.Number)) - return + c.JSON(http.StatusOK, build) } -// GetPullRequest accepts a requests to retvie a pull request -// from the datastore for the given repository and -// pull request number -// -// GET /gitlab/:owner/:name/pulls/:number -// -// REASON: It required by GitLab, becuase we get only -// sha and ref name, but drone uses build numbers func GetPullRequest(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) + db := context.Database(c) + repo := session.Repo(c) + refs := fmt.Sprintf("refs/pull/%s/head", c.Param("number")) parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { return repo.Hash, nil }) if err != nil { - c.Fail(400, err) + c.AbortWithError(http.StatusBadRequest, err) return } if parsed.Text != repo.FullName { - c.AbortWithStatus(403) + c.AbortWithStatus(http.StatusUnauthorized) return } - num, err := strconv.Atoi(c.Params.ByName("number")) - if err != nil { - c.Fail(400, err) - return - } - build, err := store.BuildPullRequestNumber(repo, num) + build, err := model.GetBuildRef(db, repo, refs) if err != nil { - c.Fail(404, err) + c.AbortWithError(http.StatusNotFound, err) return } - build.Jobs, err = store.JobList(build) - if err != nil { - c.Fail(404, err) - } else { - c.JSON(200, build) - } -} -// GetCommit accepts a requests to retvie a sha and branch -// from the datastore for the given repository and -// pull request number -// -// GET /gitlab/:owner/:name/commits/:sha -// -// REASON: It required by GitLab, becuase we get only -// sha and ref name, but drone uses build numbers -func GetCommit(c *gin.Context) { - var branch string - - store := ToDatastore(c) - repo := ToRepo(c) - sha := c.Params.ByName("sha") + c.JSON(http.StatusOK, build) +} - parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { - return repo.Hash, nil - }) - if err != nil { - c.Fail(400, err) - return - } - if parsed.Text != repo.FullName { - c.AbortWithStatus(403) - return - } +func RedirectSha(c *gin.Context) { + db := context.Database(c) + repo := session.Repo(c) - branch = c.Request.FormValue("branch") - if branch == "" { + commit := c.Param("sha") + branch := c.Query("branch") + if len(branch) == 0 { branch = repo.Branch } - build, err := store.BuildSha(repo, sha, branch) + build, err := model.GetBuildCommit(db, repo, commit, branch) if err != nil { - c.Fail(404, err) + c.AbortWithError(http.StatusNotFound, err) return } - build.Jobs, err = store.JobList(build) + path := fmt.Sprintf("/%s/%s/%d", repo.Owner, repo.Name, build.Number) + c.Redirect(http.StatusSeeOther, path) +} + +func RedirectPullRequest(c *gin.Context) { + db := context.Database(c) + repo := session.Repo(c) + refs := fmt.Sprintf("refs/pull/%s/head", c.Param("number")) + + build, err := model.GetBuildRef(db, repo, refs) if err != nil { - c.Fail(404, err) - } else { - c.JSON(200, build) + c.AbortWithError(http.StatusNotFound, err) + return } - return + path := fmt.Sprintf("/%s/%s/%d", repo.Owner, repo.Name, build.Number) + c.Redirect(http.StatusSeeOther, path) } diff --git a/controller/hooks.go b/controller/hook.go similarity index 51% rename from controller/hooks.go rename to controller/hook.go index 81d4019773..65b7d0e0cf 100644 --- a/controller/hooks.go +++ b/controller/hook.go @@ -1,40 +1,37 @@ -package server +package controller import ( + "fmt" + "github.com/gin-gonic/gin" "os" + "path/filepath" "strings" - log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/pkg/queue" - "github.com/drone/drone/pkg/token" - common "github.com/drone/drone/pkg/types" - "github.com/drone/drone/pkg/utils/httputil" - "github.com/drone/drone/pkg/yaml" - "github.com/drone/drone/pkg/yaml/matrix" + log "github.com/Sirupsen/logrus" + "github.com/drone/drone/engine" + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/shared/httputil" + "github.com/drone/drone/shared/token" + "github.com/drone/drone/yaml" + "github.com/drone/drone/yaml/matrix" ) -// PostHook accepts a post-commit hook and parses the payload -// in order to trigger a build. -// -// GET /api/hook -// func PostHook(c *gin.Context) { - remote := ToRemote(c) - store := ToDatastore(c) - queue_ := ToQueue(c) + remote := context.Remote(c) + db := context.Database(c) - hook, err := remote.Hook(c.Request) + tmprepo, build, err := remote.Hook(c.Request) if err != nil { log.Errorf("failure to parse hook. %s", err) - c.Fail(400, err) + c.AbortWithError(400, err) return } - if hook == nil { + if build == nil { c.Writer.WriteHeader(200) return } - if hook.Repo == nil { + if tmprepo == nil { log.Errorf("failure to ascertain repo from hook.") c.Writer.WriteHeader(400) return @@ -42,16 +39,16 @@ func PostHook(c *gin.Context) { // a build may be skipped if the text [CI SKIP] // is found inside the commit message - if hook.Commit != nil && strings.Contains(hook.Commit.Message, "[CI SKIP]") { + if strings.Contains(build.Message, "[CI SKIP]") { log.Infof("ignoring hook. [ci skip] found for %s") c.Writer.WriteHeader(204) return } - repo, err := store.RepoName(hook.Repo.Owner, hook.Repo.Name) + repo, err := model.GetRepoName(db, tmprepo.Owner, tmprepo.Name) if err != nil { - log.Errorf("failure to find repo %s/%s from hook. %s", hook.Repo.Owner, hook.Repo.Name, err) - c.Fail(404, err) + log.Errorf("failure to find repo %s/%s from hook. %s", tmprepo.Owner, tmprepo.Name, err) + c.AbortWithError(404, err) return } @@ -61,7 +58,7 @@ func PostHook(c *gin.Context) { }) if err != nil { log.Errorf("failure to parse token from hook for %s. %s", repo.FullName, err) - c.Fail(400, err) + c.AbortWithError(400, err) return } if parsed.Text != repo.FullName { @@ -70,106 +67,134 @@ func PostHook(c *gin.Context) { return } - switch { - case repo.UserID == 0: + if repo.UserID == 0 { log.Warnf("ignoring hook. repo %s has no owner.", repo.FullName) c.Writer.WriteHeader(204) return - case !repo.Hooks.Push && hook.Commit != nil: - log.Infof("ignoring hook. repo %s is disabled.", repo.FullName) - c.Writer.WriteHeader(204) - return - case !repo.Hooks.PullRequest && hook.PullRequest != nil: - log.Warnf("ignoring hook. repo %s is disabled for pull requests.", repo.FullName) + } + var skipped = true + if (build.Event == model.EventPush && repo.AllowPush) || + (build.Event == model.EventPull && repo.AllowPull) || + (build.Event == model.EventDeploy && repo.AllowDeploy) || + (build.Event == model.EventTag && repo.AllowTag) { + skipped = false + } + + if skipped { + log.Infof("ignoring hook. repo %s is disabled for %s events.", repo.FullName, build.Event) c.Writer.WriteHeader(204) return } - user, err := store.User(repo.UserID) + user, err := model.GetUser(db, repo.UserID) if err != nil { log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) - c.Fail(500, err) + c.AbortWithError(500, err) return } - build := &common.Build{} - build.Commit = hook.Commit - build.PullRequest = hook.PullRequest - build.Status = common.StatePending - build.RepoID = repo.ID - // fetch the .drone.yml file from the database raw, sec, err := remote.Script(user, repo, build) if err != nil { log.Errorf("failure to get .drone.yml for %s. %s", repo.FullName, err) - c.Fail(404, err) + c.AbortWithError(404, err) return } axes, err := matrix.Parse(string(raw)) if err != nil { log.Errorf("failure to calculate matrix for %s. %s", repo.FullName, err) - c.Fail(400, err) + c.AbortWithError(400, err) return } if len(axes) == 0 { axes = append(axes, matrix.Axis{}) } - for num, axis := range axes { - build.Jobs = append(build.Jobs, &common.Job{ - BuildID: build.ID, - Number: num + 1, - Status: common.StatePending, - Environment: axis, - }) - } netrc, err := remote.Netrc(user, repo) if err != nil { log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) - c.Fail(500, err) + c.AbortWithError(500, err) return } + key, _ := model.GetKey(db, repo) + // verify the branches can be built vs skipped - when, _ := parser.ParseCondition(string(raw)) - if build.PullRequest != nil && when != nil && !when.MatchBranch(build.Commit.Branch) { - log.Infof("ignoring hook. yaml file excludes repo and branch %s %s", repo.FullName, build.Commit.Branch) + yconfig, _ := yaml.Parse(string(raw)) + var match = false + for _, branch := range yconfig.Branches { + if branch == build.Branch { + match = true + break + } + match, _ = filepath.Match(branch, build.Branch) + if match { + break + } + } + if !match && len(yconfig.Branches) != 0 { + log.Infof("ignoring hook. yaml file excludes repo and branch %s %s", repo.FullName, build.Branch) c.AbortWithStatus(200) return } + tx, err := db.Begin() + if err != nil { + log.Errorf("failure to begin database transaction", err) + c.AbortWithError(500, err) + return + } + defer tx.Rollback() - err = store.AddBuild(build) + // update some build fields + build.Status = model.StatusPending + build.RepoID = repo.ID + + var jobs []*model.Job + for num, axis := range axes { + jobs = append(jobs, &model.Job{ + BuildID: build.ID, + Number: num + 1, + Status: model.StatusPending, + Environment: axis, + }) + } + err = model.CreateBuild(tx, build, jobs...) if err != nil { log.Errorf("failure to save commit for %s. %s", repo.FullName, err) - c.Fail(500, err) + c.AbortWithError(500, err) return } + tx.Commit() c.JSON(200, build) - err = remote.Status(user, repo, build) + url := fmt.Sprintf("%s/%s/%d", httputil.GetURL(c.Request), repo.FullName, build.Number) + err = remote.Status(user, repo, build, url) if err != nil { log.Errorf("error setting commit status for %s/%d", repo.FullName, build.Number) } // get the previous build so taht we can send // on status change notifications - last, _ := store.BuildLast(repo, build.Commit.Branch) + last, _ := model.GetBuildLast(db, repo, build.Branch) - queue_.Publish(&queue.Work{ + engine_ := context.Engine(c) + go engine_.Schedule(&engine.Task{ User: user, Repo: repo, Build: build, BuildPrev: last, - Keys: repo.Keys, + Jobs: jobs, + Keys: key, Netrc: netrc, - Config: raw, - Secret: sec, - System: &common.System{ + Config: string(raw), + Secret: string(sec), + System: &model.System{ Link: httputil.GetURL(c.Request), Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), }, }) + } diff --git a/controller/index.go b/controller/index.go new file mode 100644 index 0000000000..60bd6456db --- /dev/null +++ b/controller/index.go @@ -0,0 +1,199 @@ +package controller + +import ( + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/router/middleware/session" + "github.com/drone/drone/shared/httputil" + "github.com/drone/drone/shared/token" +) + +func ShowIndex(c *gin.Context) { + // remote := context.Remote(c) + user := session.User(c) + if user == nil { + c.HTML(200, "login.html", gin.H{}) + return + } + + // attempt to get the repository list from the + // cache since the operation is expensive + // v, ok := cache.Get(user.Login) + // if ok { + // c.HTML(200, "repos.html", gin.H{ + // "User": user, + // "Repos": v, + // }) + // return + // } + + // fetch the repmote repos + // repos, err := remote.Repos(user) + // if err != nil { + // c.AbortWithStatus(http.StatusInternalServerError) + // return + // } + // cache.Add(user.Login, repos) + + c.HTML(200, "repos.html", gin.H{ + "User": user, + // "Repos": repos, + }) +} + +func ShowLogin(c *gin.Context) { + c.HTML(200, "login.html", gin.H{"Error": c.Query("error")}) +} + +func ShowUser(c *gin.Context) { + user := session.User(c) + token, _ := token.New( + token.CsrfToken, + user.Login, + ).Sign(user.Hash) + + c.HTML(200, "user.html", gin.H{ + "User": user, + "Csrf": token, + }) +} + +func ShowUsers(c *gin.Context) { + db := context.Database(c) + user := session.User(c) + if !user.Admin { + c.AbortWithStatus(http.StatusForbidden) + return + } + users, _ := model.GetUserList(db) + + token, _ := token.New( + token.CsrfToken, + user.Login, + ).Sign(user.Hash) + + c.HTML(200, "users.html", gin.H{ + "User": user, + "Users": users, + "Csrf": token, + }) +} + +func ShowRepo(c *gin.Context) { + db := context.Database(c) + user := session.User(c) + repo := session.Repo(c) + if !user.Admin { + c.AbortWithStatus(http.StatusForbidden) + return + } + builds, _ := model.GetBuildList(db, repo) + groups := []*model.BuildGroup{} + + var curr *model.BuildGroup + for _, build := range builds { + date := time.Unix(build.Created, 0).Format("Jan 2 2006") + if curr == nil || curr.Date != date { + curr = &model.BuildGroup{} + curr.Date = date + groups = append(groups, curr) + } + curr.Builds = append(curr.Builds, build) + } + + httputil.SetCookie(c.Writer, c.Request, "user_last", repo.FullName) + + c.HTML(200, "repo.html", gin.H{ + "User": user, + "Repo": repo, + "Builds": builds, + "Groups": groups, + }) + +} + +func ShowRepoConf(c *gin.Context) { + db := context.Database(c) + user := session.User(c) + repo := session.Repo(c) + key, _ := model.GetKey(db, repo) + if !user.Admin { + c.AbortWithStatus(http.StatusForbidden) + return + } + var view = "repo_config.html" + switch c.Param("action") { + case "delete": + view = "repo_delete.html" + case "encrypt": + view = "repo_secret.html" + case "badges": + view = "repo_badge.html" + } + + token, _ := token.New( + token.CsrfToken, + user.Login, + ).Sign(user.Hash) + + c.HTML(200, view, gin.H{ + "User": user, + "Repo": repo, + "Key": key, + "Csrf": token, + "Link": httputil.GetURL(c.Request), + }) +} + +func ShowBuild(c *gin.Context) { + db := context.Database(c) + user := session.User(c) + repo := session.Repo(c) + num, _ := strconv.Atoi(c.Param("number")) + seq, _ := strconv.Atoi(c.Param("job")) + if seq == 0 { + seq = 1 + } + + build, err := model.GetBuildNumber(db, repo, num) + if err != nil { + c.AbortWithError(404, err) + return + } + + jobs, err := model.GetJobList(db, build) + if err != nil { + c.AbortWithError(404, err) + return + } + + var job *model.Job + for _, j := range jobs { + if j.Number == seq { + job = j + break + } + } + + httputil.SetCookie(c.Writer, c.Request, "user_last", repo.FullName) + + token, _ := token.New( + token.CsrfToken, + user.Login, + ).Sign(user.Hash) + + c.HTML(200, "build.html", gin.H{ + "User": user, + "Repo": repo, + "Build": build, + "Jobs": jobs, + "Job": job, + "Csrf": token, + }) +} diff --git a/controller/login.go b/controller/login.go index bffe5f645b..d4ebd545e6 100644 --- a/controller/login.go +++ b/controller/login.go @@ -1,82 +1,72 @@ -package server +package controller import ( + "net/http" "time" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/ungerik/go-gravatar" + "github.com/gin-gonic/gin" - log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus" - "github.com/drone/drone/pkg/token" - "github.com/drone/drone/pkg/types" + log "github.com/Sirupsen/logrus" + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/shared/crypto" + "github.com/drone/drone/shared/httputil" + "github.com/drone/drone/shared/token" ) -// GetLogin accepts a request to authorize the user and to -// return a valid OAuth2 access token. The access token is -// returned as url segment #access_token -// -// GET /authorize -// func GetLogin(c *gin.Context) { - remote := ToRemote(c) - store := ToDatastore(c) + db := context.Database(c) + remote := context.Remote(c) // when dealing with redirects we may need // to adjust the content type. I cannot, however, // rememver why, so need to revisit this line. c.Writer.Header().Del("Content-Type") - // TODO: move this back to the remote section - getLoginOauth2(c) - - // exit if authorization fails - if c.Writer.Status() != 200 { + tmpuser, open, err := remote.Login(c.Writer, c.Request) + if err != nil { + log.Errorf("cannot authenticate user. %s", err) + c.Redirect(303, "/login?error=oauth_error") return } - - login := ToUser(c) - - // check organization membership, if applicable - if len(remote.GetOrgs()) != 0 { - orgs, _ := remote.Orgs(login) - if !checkMembership(orgs, remote.GetOrgs()) { - c.Redirect(303, "/login#error=access_denied_org") - return - } + // this will happen when the user is redirected by + // the remote provide as part of the oauth dance. + if tmpuser == nil { + return } // get the user from the database - u, err := store.UserLogin(login.Login) + u, err := model.GetUserLogin(db, tmpuser.Login) if err != nil { - count, err := store.UserCount() + count, err := model.GetUserCount(db) if err != nil { - log.Errorf("cannot register %s. %s", login.Login, err) - c.Redirect(303, "/login#error=internal_error") + log.Errorf("cannot register %s. %s", tmpuser.Login, err) + c.Redirect(303, "/login?error=internal_error") return } // if self-registration is disabled we should // return a notAuthorized error. the only exception // is if no users exist yet in the system we'll proceed. - if !remote.GetOpen() && count != 0 { - log.Errorf("cannot register %s. registration closed", login.Login) - c.Redirect(303, "/login#error=access_denied") + if !open && count != 0 { + log.Errorf("cannot register %s. registration closed", tmpuser.Login) + c.Redirect(303, "/login?error=access_denied") return } // create the user account - u = &types.User{} - u.Login = login.Login - u.Token = login.Token - u.Secret = login.Secret - u.Email = login.Email - u.Avatar = login.Avatar - u.Hash = types.GenerateToken() + u = &model.User{} + u.Login = tmpuser.Login + u.Token = tmpuser.Token + u.Secret = tmpuser.Secret + u.Email = tmpuser.Email + u.Avatar = tmpuser.Avatar + u.Hash = crypto.Rand() // insert the user into the database - if err := store.AddUser(u); err != nil { - log.Errorf("cannot insert %s. %s", login.Login, err) - c.Redirect(303, "/login#error=internal_error") + if err := model.CreateUser(db, u); err != nil { + log.Errorf("cannot insert %s. %s", u.Login, err) + c.Redirect(303, "/login?error=internal_error") return } @@ -89,20 +79,14 @@ func GetLogin(c *gin.Context) { // update the user meta data and authorization // data and cache in the datastore. - u.Token = login.Token - u.Secret = login.Secret - u.Email = login.Email - u.Avatar = login.Avatar - - // TODO: remove this once gitlab implements setting - // avatar in the remote package, similar to github - if len(u.Avatar) == 0 { - u.Avatar = gravatar.Hash(u.Email) - } + u.Token = tmpuser.Token + u.Secret = tmpuser.Secret + u.Email = tmpuser.Email + u.Avatar = tmpuser.Avatar - if err := store.SetUser(u); err != nil { + if err := model.UpdateUser(db, u); err != nil { log.Errorf("cannot update %s. %s", u.Login, err) - c.Redirect(303, "/login#error=internal_error") + c.Redirect(303, "/login?error=internal_error") return } @@ -111,93 +95,65 @@ func GetLogin(c *gin.Context) { tokenstr, err := token.SignExpires(u.Hash, exp) if err != nil { log.Errorf("cannot create token for %s. %s", u.Login, err) - c.Redirect(303, "/login#error=internal_error") + c.Redirect(303, "/login?error=internal_error") return } - c.Redirect(303, "/#access_token="+tokenstr) + + httputil.SetCookie(c.Writer, c.Request, "user_sess", tokenstr) + redirect := httputil.GetCookie(c.Request, "user_last") + if len(redirect) == 0 { + redirect = "/" + } + c.Redirect(303, redirect) + } -// getLoginOauth2 is the default authorization implementation -// using the oauth2 protocol. -func getLoginOauth2(c *gin.Context) { - var remote = ToRemote(c) - - // Bugagazavr: I think this must be moved to remote config - //var scope = strings.Join(settings.Auth.Scope, ",") - //if scope == "" { - // scope = remote.Scope() - //} - var transport = remote.Oauth2Transport(c.Request) - - // get the OAuth code - var code = c.Request.FormValue("code") - //var state = c.Request.FormValue("state") - if len(code) == 0 { - // TODO this should be a random number, verified by a cookie - c.Redirect(303, transport.AuthCodeURL("random")) +func GetLogout(c *gin.Context) { + + httputil.DelCookie(c.Writer, c.Request, "user_sess") + httputil.DelCookie(c.Writer, c.Request, "user_last") + c.Redirect(303, "/login") +} + +func GetLoginToken(c *gin.Context) { + db := context.Database(c) + remote := context.Remote(c) + + in := &tokenPayload{} + err := c.Bind(in) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) return } - // exhange for a token - var token, err = transport.Exchange(code) + login, err := remote.Auth(in.Access, in.Refresh) if err != nil { - log.Errorf("cannot get access_token. %s", err) - c.Redirect(303, "/login#error=token_exchange") + c.AbortWithError(http.StatusUnauthorized, err) return } - // get user account - user, err := remote.Login(token.AccessToken, token.RefreshToken) + user, err := model.GetUserLogin(db, login) if err != nil { - log.Errorf("cannot get user with access_token. %s", err) - c.Redirect(303, "/login#error=user_not_found") + c.AbortWithError(http.StatusNotFound, err) return } - // add the user to the request - c.Set("user", user) -} - -// getLoginOauth1 is able to authorize a user with the oauth1 -// authentication protocol. This is used primarily with Bitbucket -// and Stash only, and one day I hope can be removed. -func getLoginOauth1(c *gin.Context) { - -} - -// getLoginBasic is able to authorize a user with a username and -// password. This can be used for systems that do not support oauth. -func getLoginBasic(c *gin.Context) { - var ( - remote = ToRemote(c) - username = c.Request.FormValue("username") - password = c.Request.FormValue("password") - ) - - // get user account - user, err := remote.Login(username, password) + exp := time.Now().Add(time.Hour * 72).Unix() + token := token.New(token.SessToken, user.Login) + tokenstr, err := token.SignExpires(user.Hash, exp) if err != nil { - log.Errorf("invalid username or password for %s. %s", username, err) - c.Redirect(303, "/login#error=invalid_credentials") + c.AbortWithError(http.StatusInternalServerError, err) return } - // add the user to the request - c.Set("user", user) + c.IndentedJSON(http.StatusOK, &tokenPayload{ + Access: tokenstr, + Expires: exp - time.Now().Unix(), + }) } -// checkMembership is a helper function that compares the user's -// organization list to a whitelist of organizations that are -// approved to use the system. -func checkMembership(orgs, whitelist []string) bool { - orgs_ := make(map[string]struct{}, len(orgs)) - for _, org := range orgs { - orgs_[org] = struct{}{} - } - for _, org := range whitelist { - if _, ok := orgs_[org]; ok { - return true - } - } - return false +type tokenPayload struct { + Access string `json:"access_token,omitempty"` + Refresh string `json:"refresh_token,omitempty"` + Expires int64 `json:"expires_in,omitempty"` } diff --git a/controller/node.go b/controller/node.go new file mode 100644 index 0000000000..7adcf30d54 --- /dev/null +++ b/controller/node.go @@ -0,0 +1,80 @@ +package controller + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/router/middleware/session" + "github.com/drone/drone/shared/token" +) + +func GetNodes(c *gin.Context) { + db := context.Database(c) + nodes, err := model.GetNodeList(db) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + } else { + c.IndentedJSON(http.StatusOK, nodes) + } +} + +func ShowNodes(c *gin.Context) { + db := context.Database(c) + user := session.User(c) + nodes, _ := model.GetNodeList(db) + token, _ := token.New(token.CsrfToken, user.Login).Sign(user.Hash) + c.HTML(http.StatusOK, "nodes.html", gin.H{"User": user, "Nodes": nodes, "Csrf": token}) +} + +func GetNode(c *gin.Context) { + +} + +func PostNode(c *gin.Context) { + db := context.Database(c) + engine := context.Engine(c) + + node := &model.Node{} + err := c.Bind(node) + if err != nil { + c.AbortWithStatus(http.StatusBadRequest) + return + } + node.Arch = "linux_amd64" + + err = model.InsertNode(db, node) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + ok := engine.Allocate(node) + if !ok { + c.AbortWithStatus(http.StatusInternalServerError) + } else { + c.IndentedJSON(http.StatusOK, node) + } + +} + +func DeleteNode(c *gin.Context) { + db := context.Database(c) + engine := context.Engine(c) + + id, _ := strconv.Atoi(c.Param("node")) + node, err := model.GetNode(db, int64(id)) + if err != nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + err = model.DeleteNode(db, node) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + return + } + engine.Deallocate(node) +} diff --git a/controller/queue.go b/controller/queue.go deleted file mode 100644 index 0a49be4a9a..0000000000 --- a/controller/queue.go +++ /dev/null @@ -1,11 +0,0 @@ -package server - -import ( - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" -) - -func GetQueue(c *gin.Context) { - queue := ToQueue(c) - items := queue.Items() - c.JSON(200, items) -} diff --git a/controller/recorder/recorder.go b/controller/recorder/recorder.go deleted file mode 100644 index c1c37289dd..0000000000 --- a/controller/recorder/recorder.go +++ /dev/null @@ -1,33 +0,0 @@ -package recorder - -import ( - "bufio" - "net" - "net/http" - "net/http/httptest" -) - -type ResponseRecorder struct { - *httptest.ResponseRecorder -} - -func New() *ResponseRecorder { - return &ResponseRecorder{httptest.NewRecorder()} -} - -func (rr *ResponseRecorder) reset() { - rr.ResponseRecorder = httptest.NewRecorder() -} - -func (rr *ResponseRecorder) CloseNotify() <-chan bool { - return http.ResponseWriter(rr).(http.CloseNotifier).CloseNotify() -} - -func (rr *ResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { - return http.ResponseWriter(rr).(http.Hijacker).Hijack() -} - -func (rr *ResponseRecorder) Size() int { return rr.Body.Len() } -func (rr *ResponseRecorder) Status() int { return rr.Code } -func (rr *ResponseRecorder) WriteHeaderNow() {} -func (rr *ResponseRecorder) Written() bool { return rr.Code != 0 } diff --git a/controller/repo.go b/controller/repo.go new file mode 100644 index 0000000000..d1589d2469 --- /dev/null +++ b/controller/repo.go @@ -0,0 +1,274 @@ +package controller + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + + "github.com/gin-gonic/gin" + "gopkg.in/yaml.v2" + + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/router/middleware/session" + "github.com/drone/drone/shared/crypto" + "github.com/drone/drone/shared/httputil" + "github.com/drone/drone/shared/token" +) + +func PostRepo(c *gin.Context) { + db := context.Database(c) + remote := context.Remote(c) + user := session.User(c) + owner := c.Param("owner") + name := c.Param("name") + + if user == nil { + c.AbortWithStatus(403) + return + } + + r, err := remote.Repo(user, owner, name) + if err != nil { + c.String(404, err.Error()) + return + } + m, err := remote.Perm(user, owner, name) + if err != nil { + c.String(404, err.Error()) + return + } + if !m.Admin { + c.String(403, "Administrative access is required.") + return + } + + // error if the repository already exists + _, err = model.GetRepoName(db, owner, name) + if err == nil { + c.String(409, "Repository already exists.") + return + } + + // set the repository owner to the + // currently authenticated user. + r.UserID = user.ID + r.AllowPush = true + r.AllowPull = true + r.Timeout = 60 // 1 hour default build time + r.Hash = crypto.Rand() + + // crates the jwt token used to verify the repository + t := token.New(token.HookToken, r.FullName) + sig, err := t.Sign(r.Hash) + if err != nil { + c.AbortWithError(500, err) + return + } + + link := fmt.Sprintf( + "%s/hook?access_token=%s", + httputil.GetURL(c.Request), + sig, + ) + + // generate an RSA key and add to the repo + key, err := crypto.GeneratePrivateKey() + if err != nil { + c.AbortWithError(500, err) + return + } + keys := new(model.Key) + keys.Public = string(crypto.MarshalPublicKey(&key.PublicKey)) + keys.Private = string(crypto.MarshalPrivateKey(key)) + + // activate the repository before we make any + // local changes to the database. + err = remote.Activate(user, r, keys, link) + if err != nil { + c.AbortWithError(500, err) + return + } + + tx, err := db.Begin() + if err != nil { + c.AbortWithError(500, err) + return + } + defer tx.Rollback() + + // persist the repository + err = model.CreateRepo(tx, r) + if err != nil { + c.AbortWithError(500, err) + return + } + keys.RepoID = r.ID + err = model.CreateKey(tx, keys) + if err != nil { + c.AbortWithError(500, err) + return + } + err = model.CreateStar(tx, user, r) + if err != nil { + c.AbortWithError(500, err) + return + } + tx.Commit() + + c.JSON(200, r) +} + +func PatchRepo(c *gin.Context) { + db := context.Database(c) + repo := session.Repo(c) + user := session.User(c) + + in := &struct { + IsTrusted *bool `json:"trusted,omitempty"` + Timeout *int64 `json:"timeout,omitempty"` + AllowPull *bool `json:"allow_pr,omitempty"` + AllowPush *bool `json:"allow_push,omitempty"` + AllowDeploy *bool `json:"allow_deploy,omitempty"` + AllowTag *bool `json:"allow_tag,omitempty"` + }{} + if err := c.Bind(in); err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + if in.AllowPush != nil { + repo.AllowPush = *in.AllowPush + } + if in.AllowPull != nil { + repo.AllowPull = *in.AllowPull + } + if in.AllowDeploy != nil { + repo.AllowDeploy = *in.AllowDeploy + } + if in.AllowTag != nil { + repo.AllowTag = *in.AllowTag + } + if in.IsTrusted != nil && user.Admin { + repo.IsTrusted = *in.IsTrusted + } + if in.Timeout != nil && user.Admin { + repo.Timeout = *in.Timeout + } + + err := model.UpdateRepo(db, repo) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + // if the user is authenticated we should + // check to see if they've starred the repository + repo.IsStarred, _ = model.GetStar(db, user, repo) + + c.IndentedJSON(http.StatusOK, repo) +} + +func GetRepo(c *gin.Context) { + db := context.Database(c) + repo := session.Repo(c) + user := session.User(c) + if user == nil { + c.IndentedJSON(http.StatusOK, repo) + return + } + + // if the user is authenticated we should + // check to see if they've starred the repository + repo.IsStarred, _ = model.GetStar(db, user, repo) + + c.IndentedJSON(http.StatusOK, repo) +} + +func GetRepoKey(c *gin.Context) { + db := context.Database(c) + repo := session.Repo(c) + keys, err := model.GetKey(db, repo) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + } else { + c.String(http.StatusOK, keys.Public) + } +} + +func DeleteRepo(c *gin.Context) { + db := context.Database(c) + repo := session.Repo(c) + + err := model.DeleteRepo(db, repo) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + } else { + c.Writer.WriteHeader(http.StatusOK) + } +} + +func PostSecure(c *gin.Context) { + db := context.Database(c) + repo := session.Repo(c) + + in, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + // we found some strange characters included in + // the yaml file when entered into a browser textarea. + // these need to be removed + in = bytes.Replace(in, []byte{'\xA0'}, []byte{' '}, -1) + + // make sure the Yaml is valid format to prevent + // a malformed value from being used in the build + err = yaml.Unmarshal(in, &yaml.MapSlice{}) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) + return + } + + key, err := model.GetKey(db, repo) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + // encrypts using go-jose + out, err := crypto.Encrypt(string(in), key.Private) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + c.String(http.StatusOK, out) +} + +func PostStar(c *gin.Context) { + db := context.Database(c) + repo := session.Repo(c) + user := session.User(c) + + err := model.CreateStar(db, user, repo) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + } else { + c.Writer.WriteHeader(http.StatusOK) + } +} + +func DeleteStar(c *gin.Context) { + db := context.Database(c) + repo := session.Repo(c) + user := session.User(c) + + err := model.DeleteStar(db, user, repo) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + } else { + c.Writer.WriteHeader(http.StatusOK) + } +} diff --git a/controller/repos.go b/controller/repos.go deleted file mode 100644 index efe5f1bbc7..0000000000 --- a/controller/repos.go +++ /dev/null @@ -1,349 +0,0 @@ -package server - -import ( - "bytes" - "encoding/base64" - "fmt" - "io/ioutil" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin/binding" - "github.com/drone/drone/Godeps/_workspace/src/gopkg.in/yaml.v2" - - "github.com/drone/drone/pkg/remote" - "github.com/drone/drone/pkg/token" - common "github.com/drone/drone/pkg/types" - "github.com/drone/drone/pkg/utils/httputil" - "github.com/drone/drone/pkg/utils/sshutil" - "github.com/drone/drone/pkg/yaml/secure" -) - -// repoResp is a data structure used for sending -// repository data to the client, augmented with -// additional repository meta-data. -type repoResp struct { - *common.Repo - Perms *common.Perm `json:"permissions,omitempty"` - Keypair *common.Keypair `json:"keypair,omitempty"` - Params map[string]string `json:"params,omitempty"` - Starred bool `json:"starred,omitempty"` -} - -// repoReq is a data structure used for receiving -// repository data from the client to modify the -// attributes of an existing repository. -// -// note that attributes are pointers so that we can -// accept null values, effectively patching an existing -// repository object with only the supplied fields. -type repoReq struct { - Trusted *bool `json:"trusted"` - Timeout *int64 `json:"timeout"` - - Hooks struct { - PullReqeust *bool `json:"pull_request"` - Push *bool `json:"push"` - } - - // optional private parameters can only be - // supplied by the repository admin. - Params *map[string]string `json:"params"` -} - -// GetRepo accepts a request to retrieve a commit -// from the datastore for the given repository, branch and -// commit hash. -// -// GET /api/repos/:owner/:name -// -func GetRepo(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - user := ToUser(c) - perm := ToPerm(c) - data := repoResp{repo, perm, nil, nil, false} - - // if the user is authenticated, we should display - // if she is watching the current repository. - if user == nil { - c.JSON(200, data) - return - } - - // if the user is an administrator of the project - // we should display the private parameter data - // and keypair data. - if perm.Push { - data.Params = repo.Params - data.Keypair = repo.Keys - } - // check to see if the user is subscribing to the repo - data.Starred, _ = store.Starred(user, repo) - - c.JSON(200, data) -} - -// PutRepo accepts a request to update the named repository -// in the datastore. It expects a JSON input and returns the -// updated repository in JSON format if successful. -// -// PUT /api/repos/:owner/:name -// -func PutRepo(c *gin.Context) { - store := ToDatastore(c) - perm := ToPerm(c) - user := ToUser(c) - repo := ToRepo(c) - - in := &repoReq{} - if !c.BindWith(in, binding.JSON) { - return - } - - if in.Params != nil { - repo.Params = *in.Params - } - - if in.Hooks.Push != nil { - repo.Hooks.Push = *in.Hooks.Push - } - if in.Hooks.PullReqeust != nil { - repo.Hooks.PullRequest = *in.Hooks.PullReqeust - } - if in.Trusted != nil && user.Admin { - repo.Trusted = *in.Trusted - } - if in.Timeout != nil && user.Admin { - repo.Timeout = *in.Timeout - } - - err := store.SetRepo(repo) - if err != nil { - c.Fail(400, err) - return - } - - data := repoResp{repo, perm, nil, nil, false} - data.Params = repo.Params - data.Keypair = repo.Keys - data.Starred, _ = store.Starred(user, repo) - - c.JSON(200, data) -} - -// DeleteRepo accepts a request to delete the named -// repository. -// -// DEL /api/repos/:owner/:name -// -func DeleteRepo(c *gin.Context) { - ds := ToDatastore(c) - u := ToUser(c) - r := ToRepo(c) - - link := fmt.Sprintf( - "%s/api/hook", - httputil.GetURL(c.Request), - ) - - remote := ToRemote(c) - err := remote.Deactivate(u, r, link) - if err != nil { - c.Fail(400, err) - } - - err = ds.DelRepo(r) - if err != nil { - c.Fail(500, err) - } - c.Writer.WriteHeader(200) -} - -// PostRepo accapets a request to activate the named repository -// in the datastore. It returns a 201 status created if successful -// -// POST /api/repos/:owner/:name -// -func PostRepo(c *gin.Context) { - user := ToUser(c) - store := ToDatastore(c) - owner := c.Params.ByName("owner") - name := c.Params.ByName("name") - - // get the repository and user permissions - // from the remote system. - remote := ToRemote(c) - r, err := remote.Repo(user, owner, name) - if err != nil { - c.Fail(404, err) - } - m, err := remote.Perm(user, owner, name) - if err != nil { - c.Fail(404, err) - return - } - if !m.Admin { - c.Fail(403, fmt.Errorf("must be repository admin")) - return - } - - // error if the repository already exists - _, err = store.RepoName(owner, name) - if err == nil { - c.String(409, "Repository already exists") - return - } - - // set the repository owner to the - // currently authenticated user. - r.UserID = user.ID - r.Hooks = new(common.Hooks) - r.Hooks.Push = true - r.Hooks.PullRequest = true - r.Timeout = 60 // 1 hour default build time - r.Hash = common.GenerateToken() - r.Self = fmt.Sprintf( - "%s/%s", - httputil.GetURL(c.Request), - r.FullName, - ) - - // crates the jwt token used to verify the repository - t := token.New(token.HookToken, r.FullName) - sig, err := t.Sign(r.Hash) - if err != nil { - c.Fail(500, err) - return - } - - link := fmt.Sprintf( - "%s/api/hook?access_token=%s", - httputil.GetURL(c.Request), - sig, - ) - - // generate an RSA key and add to the repo - key, err := sshutil.GeneratePrivateKey() - if err != nil { - c.Fail(500, err) - return - } - r.Keys = new(common.Keypair) - r.Keys.Public = string(sshutil.MarshalPublicKey(&key.PublicKey)) - r.Keys.Private = string(sshutil.MarshalPrivateKey(key)) - - // activate the repository before we make any - // local changes to the database. - err = remote.Activate(user, r, r.Keys, link) - if err != nil { - c.Fail(500, err) - return - } - - // persist the repository - err = store.AddRepo(r) - if err != nil { - c.Fail(500, err) - return - } - - store.AddStar(user, r) - - c.JSON(200, r) -} - -// Encrypt accapets a request to encrypt the -// body of the request using the repository secret -// key. -// -// POST /api/repos/:owner/:name/encrypt -// -func Encrypt(c *gin.Context) { - repo := ToRepo(c) - in, err := ioutil.ReadAll(c.Request.Body) - if err != nil { - c.Fail(400, err) - return - } - in, err = base64.StdEncoding.DecodeString(string(in)) - if err != nil { - c.Fail(500, err) - return - } - - // we found some strange characters included in - // the yaml file when entered into a browser textarea. - // these need to be removed - in = bytes.Replace(in, []byte{'\xA0'}, []byte{' '}, -1) - - // make sure the Yaml is valid format to prevent - // a malformed value from being used in the build - err = yaml.Unmarshal(in, &yaml.MapSlice{}) - if err != nil { - c.Fail(500, err) - return - } - - // encrypts using go-jose - out, err := secure.Encrypt(string(in), repo.Keys.Private) - if err != nil { - c.Fail(500, err) - return - } - c.Writer.Write([]byte(out)) -} - -// Unsubscribe accapets a request to unsubscribe the -// currently authenticated user to the repository. -// -// DEL /api/subscribers/:owner/:name -// -func Unsubscribe(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - user := ToUser(c) - - err := store.DelStar(user, repo) - if err != nil { - c.Fail(400, err) - } else { - c.Writer.WriteHeader(200) - } -} - -// Subscribe accapets a request to subscribe the -// currently authenticated user to the repository. -// -// POST /api/subscriber/:owner/:name -// -func Subscribe(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - user := ToUser(c) - - err := store.AddStar(user, repo) - if err != nil { - c.Fail(400, err) - } else { - c.Writer.WriteHeader(200) - } -} - -// perms is a helper function that returns user permissions -// for a particular repository. -func perms(remote remote.Remote, u *common.User, r *common.Repo) *common.Perm { - switch { - case u == nil && r.Private: - return &common.Perm{} - case u == nil && r.Private == false: - return &common.Perm{Pull: true} - case u.Admin: - return &common.Perm{Pull: true, Push: true, Admin: true} - } - - p, err := remote.Perm(u, r.Owner, r.Name) - if err != nil { - return &common.Perm{} - } - return p -} diff --git a/controller/server.go b/controller/server.go deleted file mode 100644 index 79519f4e03..0000000000 --- a/controller/server.go +++ /dev/null @@ -1,261 +0,0 @@ -package server - -import ( - "net/http" - "time" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - - "github.com/drone/drone/pkg/bus" - "github.com/drone/drone/pkg/queue" - "github.com/drone/drone/pkg/remote" - "github.com/drone/drone/pkg/runner" - "github.com/drone/drone/pkg/store" - "github.com/drone/drone/pkg/token" - common "github.com/drone/drone/pkg/types" -) - -func SetQueue(q queue.Queue) gin.HandlerFunc { - return func(c *gin.Context) { - c.Set("queue", q) - c.Next() - } -} - -func ToQueue(c *gin.Context) queue.Queue { - v, ok := c.Get("queue") - if !ok { - return nil - } - return v.(queue.Queue) -} - -func SetBus(r bus.Bus) gin.HandlerFunc { - return func(c *gin.Context) { - c.Set("bus", r) - c.Next() - } -} - -func ToBus(c *gin.Context) bus.Bus { - v, ok := c.Get("bus") - if !ok { - return nil - } - return v.(bus.Bus) -} - -func ToRemote(c *gin.Context) remote.Remote { - v, ok := c.Get("remote") - if !ok { - return nil - } - return v.(remote.Remote) -} - -func SetRemote(r remote.Remote) gin.HandlerFunc { - return func(c *gin.Context) { - c.Set("remote", r) - c.Next() - } -} - -func ToRunner(c *gin.Context) runner.Runner { - v, ok := c.Get("runner") - if !ok { - return nil - } - return v.(runner.Runner) -} - -func SetRunner(r runner.Runner) gin.HandlerFunc { - return func(c *gin.Context) { - c.Set("runner", r) - c.Next() - } -} - -func ToPerm(c *gin.Context) *common.Perm { - v, ok := c.Get("perm") - if !ok { - return nil - } - return v.(*common.Perm) -} - -func ToUser(c *gin.Context) *common.User { - v, ok := c.Get("user") - if !ok { - return nil - } - return v.(*common.User) -} - -func ToRepo(c *gin.Context) *common.Repo { - v, ok := c.Get("repo") - if !ok { - return nil - } - return v.(*common.Repo) -} - -func ToDatastore(c *gin.Context) store.Store { - return c.MustGet("datastore").(store.Store) -} - -func SetDatastore(ds store.Store) gin.HandlerFunc { - return func(c *gin.Context) { - c.Set("datastore", ds) - c.Next() - } -} - -func SetUser() gin.HandlerFunc { - return func(c *gin.Context) { - - var store = ToDatastore(c) - var user *common.User - - _, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { - var err error - user, err = store.UserLogin(t.Text) - if err != nil { - return "", err - } - return user.Hash, nil - }) - - if err == nil && user != nil && user.ID != 0 { - c.Set("user", user) - } - c.Next() - } -} - -func SetRepo() gin.HandlerFunc { - return func(c *gin.Context) { - ds := ToDatastore(c) - u := ToUser(c) - owner := c.Params.ByName("owner") - name := c.Params.ByName("name") - r, err := ds.RepoName(owner, name) - switch { - case err != nil && u != nil: - c.Fail(404, err) - return - case err != nil && u == nil: - c.Fail(401, err) - return - } - c.Set("repo", r) - c.Next() - } -} - -func SetPerm() gin.HandlerFunc { - return func(c *gin.Context) { - remote := ToRemote(c) - user := ToUser(c) - repo := ToRepo(c) - perm := perms(remote, user, repo) - c.Set("perm", perm) - c.Next() - } -} - -func MustUser() gin.HandlerFunc { - return func(c *gin.Context) { - u := ToUser(c) - if u == nil { - c.AbortWithStatus(401) - } else { - c.Set("user", u) - c.Next() - } - } -} - -func MustAdmin() gin.HandlerFunc { - return func(c *gin.Context) { - u := ToUser(c) - if u == nil { - c.AbortWithStatus(401) - } else if !u.Admin { - c.AbortWithStatus(403) - } else { - c.Set("user", u) - c.Next() - } - } -} - -func CheckPull() gin.HandlerFunc { - return func(c *gin.Context) { - u := ToUser(c) - m := ToPerm(c) - - switch { - case u == nil && m == nil: - c.AbortWithStatus(401) - case u == nil && m.Pull == false: - c.AbortWithStatus(401) - case u != nil && m.Pull == false: - c.AbortWithStatus(404) - default: - c.Next() - } - } -} - -func CheckPush() gin.HandlerFunc { - return func(c *gin.Context) { - switch c.Request.Method { - case "GET", "OPTIONS": - c.Next() - return - } - - u := ToUser(c) - m := ToPerm(c) - - switch { - case u == nil && m.Push == false: - c.AbortWithStatus(401) - case u != nil && m.Push == false: - c.AbortWithStatus(404) - default: - c.Next() - } - } -} - -func SetHeaders() gin.HandlerFunc { - return func(c *gin.Context) { - - c.Writer.Header().Add("Access-Control-Allow-Origin", "*") - c.Writer.Header().Add("X-Frame-Options", "DENY") - c.Writer.Header().Add("X-Content-Type-Options", "nosniff") - c.Writer.Header().Add("X-XSS-Protection", "1; mode=block") - c.Writer.Header().Add("Cache-Control", "no-cache") - c.Writer.Header().Add("Cache-Control", "no-store") - c.Writer.Header().Add("Cache-Control", "max-age=0") - c.Writer.Header().Add("Cache-Control", "must-revalidate") - c.Writer.Header().Add("Cache-Control", "value") - c.Writer.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) - c.Writer.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") - if c.Request.TLS != nil { - c.Writer.Header().Add("Strict-Transport-Security", "max-age=31536000") - } - - if c.Request.Method == "OPTIONS" { - c.Writer.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization") - c.Writer.Header().Set("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(200) - return - } - - c.Next() - } -} diff --git a/controller/stream.go b/controller/stream.go new file mode 100644 index 0000000000..87041e0aa8 --- /dev/null +++ b/controller/stream.go @@ -0,0 +1,123 @@ +package controller + +/* + stream.Get("/:owner/:name", controller.GetRepoEvents) + stream.Get("/:owner/:name/:build/:number", controller.GetStream) +*/ + +import ( + "io" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/docker/docker/pkg/stdcopy" + "github.com/drone/drone/engine" + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/router/middleware/session" + + log "github.com/Sirupsen/logrus" + + "github.com/manucorporat/sse" +) + +// GetRepoEvents will upgrade the connection to a Websocket and will stream +// event updates to the browser. +func GetRepoEvents(c *gin.Context) { + engine_ := context.Engine(c) + repo := session.Repo(c) + c.Writer.Header().Set("Content-Type", "text/event-stream") + + eventc := make(chan *engine.Event, 1) + engine_.Subscribe(eventc) + defer func() { + engine_.Unsubscribe(eventc) + close(eventc) + log.Infof("closed event stream") + }() + + c.Stream(func(w io.Writer) bool { + select { + case event := <-eventc: + if event == nil { + log.Infof("nil event received") + return false + } + if event.Name == repo.FullName { + log.Debugf("received message %s", event.Name) + sse.Encode(w, sse.Event{ + Event: "message", + Data: string(event.Msg), + }) + } + case <-c.Writer.CloseNotify(): + return false + } + return true + }) +} + +func GetStream(c *gin.Context) { + db := context.Database(c) + engine_ := context.Engine(c) + repo := session.Repo(c) + buildn, _ := strconv.Atoi(c.Param("build")) + jobn, _ := strconv.Atoi(c.Param("number")) + + c.Writer.Header().Set("Content-Type", "text/event-stream") + + build, err := model.GetBuildNumber(db, repo, buildn) + if err != nil { + log.Debugln("stream cannot get build number.", err) + c.AbortWithError(404, err) + return + } + job, err := model.GetJobNumber(db, build, jobn) + if err != nil { + log.Debugln("stream cannot get job number.", err) + c.AbortWithError(404, err) + return + } + node, err := model.GetNode(db, job.NodeID) + if err != nil { + log.Debugln("stream cannot get node.", err) + c.AbortWithError(404, err) + return + } + + rc, err := engine_.Stream(build.ID, job.ID, node) + if err != nil { + c.AbortWithError(404, err) + return + } + + defer func() { + rc.Close() + }() + + go func() { + <-c.Writer.CloseNotify() + rc.Close() + }() + + rw := &StreamWriter{c.Writer, 0} + + stdcopy.StdCopy(rw, rw, rc) +} + +type StreamWriter struct { + writer gin.ResponseWriter + count int +} + +func (w *StreamWriter) Write(data []byte) (int, error) { + var err = sse.Encode(w.writer, sse.Event{ + Id: strconv.Itoa(w.count), + Event: "message", + Data: string(data), + }) + w.writer.Flush() + w.count += len(data) + return len(data), err +} diff --git a/controller/user.go b/controller/user.go index 47399d2a5c..bc9fd26c5a 100644 --- a/controller/user.go +++ b/controller/user.go @@ -1,103 +1,82 @@ -package server +package controller import ( - // "crypto/sha1" + "net/http" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin/binding" - "github.com/drone/drone/Godeps/_workspace/src/github.com/ungerik/go-gravatar" + "github.com/gin-gonic/gin" - "github.com/drone/drone/pkg/token" - "github.com/drone/drone/pkg/types" + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/router/middleware/session" + "github.com/drone/drone/shared/token" + "github.com/hashicorp/golang-lru" ) -// GetUserCurr accepts a request to retrieve the -// currently authenticated user from the datastore -// and return in JSON format. -// -// GET /api/user -// -func GetUserCurr(c *gin.Context) { - u := ToUser(c) - // f := fmt.Printf("% x", sha1.Sum(u.Hash)) +var cache *lru.Cache - // v := struct { - // *types.User - - // // token fingerprint - // Token string `json:"token"` - // }{u, f} - - c.JSON(200, u) +func init() { + var err error + cache, err = lru.New(1028) + if err != nil { + panic(err) + } } -// PutUserCurr accepts a request to update the currently -// authenticated User profile. -// -// PUT /api/user -// -func PutUserCurr(c *gin.Context) { - store := ToDatastore(c) - user := ToUser(c) +func GetSelf(c *gin.Context) { + c.IndentedJSON(200, session.User(c)) +} - in := &types.User{} - if !c.BindWith(in, binding.JSON) { - return - } - // TODO: we are no longer auto-generating avatar - user.Email = in.Email - user.Avatar = gravatar.Hash(in.Email) - err := store.SetUser(user) +func GetFeed(c *gin.Context) { + user := session.User(c) + db := context.Database(c) + feed, err := model.GetUserFeed(db, user, 25, 0) if err != nil { - c.Fail(400, err) - } else { - c.JSON(200, user) + c.AbortWithStatus(http.StatusInternalServerError) + return } + c.IndentedJSON(http.StatusOK, feed) } -// GetUserRepos accepts a request to get the currently -// authenticated user's repository list from the datastore, -// encoded and returned in JSON format. -// -// GET /api/user/repos -// -func GetUserRepos(c *gin.Context) { - store := ToDatastore(c) - user := ToUser(c) - repos, err := store.RepoList(user) +func GetRepos(c *gin.Context) { + user := session.User(c) + db := context.Database(c) + repos, err := model.GetRepoList(db, user) if err != nil { - c.Fail(400, err) - } else { - c.JSON(200, &repos) + c.AbortWithStatus(http.StatusInternalServerError) + return } + c.IndentedJSON(http.StatusOK, repos) } -// GetUserFeed accepts a request to get the currently -// authenticated user's build feed from the datastore, -// encoded and returned in JSON format. -// -// GET /api/user/feed -// -func GetUserFeed(c *gin.Context) { - store := ToDatastore(c) - user := ToUser(c) - feed, err := store.UserFeed(user, 25, 0) +func GetRemoteRepos(c *gin.Context) { + user := session.User(c) + remote := context.Remote(c) + + // attempt to get the repository list from the + // cache since the operation is expensive + v, ok := cache.Get(user.Login) + if ok { + c.IndentedJSON(http.StatusOK, v) + return + } + + repos, err := remote.Repos(user) if err != nil { - c.Fail(400, err) - } else { - c.JSON(200, &feed) + c.AbortWithStatus(http.StatusInternalServerError) + return } + cache.Add(user.Login, repos) + c.IndentedJSON(http.StatusOK, repos) } -// POST /api/user/token -func PostUserToken(c *gin.Context) { - user := ToUser(c) +func PostToken(c *gin.Context) { + user := session.User(c) token := token.New(token.UserToken, user.Login) tokenstr, err := token.Sign(user.Hash) if err != nil { - c.Fail(500, err) + c.AbortWithError(http.StatusInternalServerError, err) } else { - c.String(200, tokenstr) + c.String(http.StatusOK, tokenstr) } } diff --git a/controller/user_test.go b/controller/user_test.go deleted file mode 100644 index f57aaecb54..0000000000 --- a/controller/user_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package server - -import ( - "bytes" - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "testing" - - . "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/pkg/server/recorder" - "github.com/drone/drone/pkg/store/mock" - common "github.com/drone/drone/pkg/types" -) - -func TestUser(t *testing.T) { - store := new(mocks.Store) - - g := Goblin(t) - g.Describe("User", func() { - - g.It("should get", func() { - rw := recorder.New() - ctx := &gin.Context{Engine: gin.Default(), Writer: rw} - - user := &common.User{Login: "octocat"} - ctx.Set("user", user) - - GetUserCurr(ctx) - - out := &common.User{} - json.NewDecoder(rw.Body).Decode(out) - g.Assert(rw.Code).Equal(200) - g.Assert(out).Equal(user) - }) - - g.It("should put", func() { - var buf bytes.Buffer - in := &common.User{Email: "octocat@github.com"} - json.NewEncoder(&buf).Encode(in) - - rw := recorder.New() - ctx := &gin.Context{Engine: gin.Default(), Writer: rw} - ctx.Request = &http.Request{Body: ioutil.NopCloser(&buf)} - ctx.Request.Header = http.Header{} - ctx.Request.Header.Set("Content-Type", "application/json") - - user := &common.User{Login: "octocat"} - ctx.Set("user", user) - ctx.Set("datastore", store) - store.On("SetUser", user).Return(nil).Once() - - PutUserCurr(ctx) - - out := &common.User{} - json.NewDecoder(rw.Body).Decode(out) - g.Assert(rw.Code).Equal(200) - g.Assert(out.Login).Equal(user.Login) - g.Assert(out.Email).Equal(in.Email) - g.Assert(out.Avatar).Equal("7194e8d48fa1d2b689f99443b767316c") - }) - - g.It("should put, error", func() { - var buf bytes.Buffer - in := &common.User{Email: "octocat@github.com"} - json.NewEncoder(&buf).Encode(in) - - rw := recorder.New() - ctx := &gin.Context{Engine: gin.Default(), Writer: rw} - ctx.Request = &http.Request{Body: ioutil.NopCloser(&buf)} - ctx.Request.Header = http.Header{} - ctx.Request.Header.Set("Content-Type", "application/json") - - user := &common.User{Login: "octocat"} - ctx.Set("user", user) - ctx.Set("datastore", store) - store.On("SetUser", user).Return(errors.New("error")).Once() - - PutUserCurr(ctx) - - out := &common.User{} - json.NewDecoder(rw.Body).Decode(out) - g.Assert(rw.Code).Equal(400) - }) - }) -} diff --git a/controller/users.go b/controller/users.go index c273618671..15be123693 100644 --- a/controller/users.go +++ b/controller/users.go @@ -1,131 +1,118 @@ -package server +package controller import ( - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin/binding" - "github.com/drone/drone/Godeps/_workspace/src/github.com/ungerik/go-gravatar" + "net/http" - "github.com/drone/drone/pkg/types" + "github.com/gin-gonic/gin" + + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/router/middleware/session" + "github.com/drone/drone/shared/crypto" ) -// GetUsers accepts a request to retrieve all users -// from the datastore and return encoded in JSON format. -// -// GET /api/users -// func GetUsers(c *gin.Context) { - store := ToDatastore(c) - users, err := store.UserList() + db := context.Database(c) + users, err := model.GetUserList(db) if err != nil { - c.Fail(400, err) - } else { - c.JSON(200, users) + c.AbortWithStatus(http.StatusInternalServerError) + return } -} -// PostUser accepts a request to create a new user in the -// system. The created user account is returned in JSON -// format if successful. -// -// POST /api/users -// -func PostUser(c *gin.Context) { - store := ToDatastore(c) - name := c.Params.ByName("name") - user := &types.User{Login: name} - user.Token = c.Request.FormValue("token") - user.Secret = c.Request.FormValue("secret") - user.Hash = c.Request.FormValue("hash") - if len(user.Hash) == 0 { - user.Hash = types.GenerateToken() - } - if err := store.AddUser(user); err != nil { - c.Fail(400, err) - } else { - c.JSON(201, user) - } + c.IndentedJSON(http.StatusOK, users) } -// GetUser accepts a request to retrieve a user by hostname -// and login from the datastore and return encoded in JSON -// format. -// -// GET /api/users/:name -// func GetUser(c *gin.Context) { - store := ToDatastore(c) - name := c.Params.ByName("name") - user, err := store.UserLogin(name) + db := context.Database(c) + user, err := model.GetUserLogin(db, c.Param("login")) if err != nil { - c.Fail(404, err) - } else { - c.JSON(200, user) + c.AbortWithStatus(http.StatusNotFound) + return } + + c.IndentedJSON(http.StatusOK, user) } -// PutUser accepts a request to update an existing user in -// the system. The modified user account is returned in JSON -// format if successful. -// -// PUT /api/users/:name -// -func PutUser(c *gin.Context) { - store := ToDatastore(c) - me := ToUser(c) - name := c.Params.ByName("name") - user, err := store.UserLogin(name) +func PatchUser(c *gin.Context) { + me := session.User(c) + db := context.Database(c) + in := &model.User{} + err := c.Bind(in) if err != nil { - c.Fail(404, err) + c.AbortWithStatus(http.StatusBadRequest) return } - in := &types.User{} - if !c.BindWith(in, binding.JSON) { + user, err := model.GetUserLogin(db, c.Param("login")) + if err != nil { + c.AbortWithStatus(http.StatusNotFound) return } - user.Email = in.Email - user.Avatar = gravatar.Hash(user.Email) + user.Admin = in.Admin + user.Active = in.Active + + // cannot update self + if me.ID == user.ID { + c.AbortWithStatus(http.StatusForbidden) + return + } + + err = model.UpdateUser(db, user) + if err != nil { + c.AbortWithStatus(http.StatusConflict) + return + } + + c.IndentedJSON(http.StatusOK, user) +} - // an administrator must not be able to - // downgrade her own account. - if me.Login != user.Login { - user.Admin = in.Admin +func PostUser(c *gin.Context) { + db := context.Database(c) + in := &model.User{} + err := c.Bind(in) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) + return } - err = store.SetUser(user) + user := &model.User{} + user.Login = in.Login + user.Email = in.Email + user.Admin = in.Admin + user.Avatar = in.Avatar + user.Active = true + user.Hash = crypto.Rand() + + err = model.CreateUser(db, user) if err != nil { - c.Fail(400, err) - } else { - c.JSON(200, user) + c.String(http.StatusInternalServerError, err.Error()) + return } + + c.IndentedJSON(http.StatusOK, user) } -// DeleteUser accepts a request to delete the specified -// user account from the system. A successful request will -// respond with an OK 200 status. -// -// DELETE /api/users/:name -// func DeleteUser(c *gin.Context) { - store := ToDatastore(c) - me := ToUser(c) - name := c.Params.ByName("name") - user, err := store.UserLogin(name) + me := session.User(c) + db := context.Database(c) + + user, err := model.GetUserLogin(db, c.Param("login")) if err != nil { - c.Fail(404, err) + c.AbortWithStatus(http.StatusNotFound) return } - // an administrator must not be able to - // delete her own account. - if user.Login == me.Login { - c.Writer.WriteHeader(403) + // cannot delete self + if me.ID == user.ID { + c.AbortWithStatus(http.StatusForbidden) return } - if err := store.DelUser(user); err != nil { - c.Fail(400, err) - } else { - c.Writer.WriteHeader(204) + err = model.DeleteUser(db, user) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + return } + + c.Writer.WriteHeader(http.StatusNoContent) } diff --git a/controller/ws.go b/controller/ws.go deleted file mode 100644 index 7a747717a6..0000000000 --- a/controller/ws.go +++ /dev/null @@ -1,105 +0,0 @@ -package server - -import ( - "io" - "strconv" - - "github.com/drone/drone/pkg/bus" - - log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/manucorporat/sse" - "github.com/drone/drone/pkg/docker" -) - -// GetRepoEvents will upgrade the connection to a Websocket and will stream -// event updates to the browser. -func GetRepoEvents(c *gin.Context) { - bus_ := ToBus(c) - repo := ToRepo(c) - c.Writer.Header().Set("Content-Type", "text/event-stream") - - eventc := make(chan *bus.Event, 1) - bus_.Subscribe(eventc) - defer func() { - bus_.Unsubscribe(eventc) - close(eventc) - log.Infof("closed event stream") - }() - - c.Stream(func(w io.Writer) bool { - select { - case event := <-eventc: - if event == nil { - log.Infof("nil event received") - return false - } - if event.Kind == bus.EventRepo && - event.Name == repo.FullName { - sse.Encode(w, sse.Event{ - Event: "message", - Data: string(event.Msg), - }) - } - case <-c.Writer.CloseNotify(): - return false - } - return true - }) -} - -func GetStream(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - runner := ToRunner(c) - commitseq, _ := strconv.Atoi(c.Params.ByName("build")) - jobnum, _ := strconv.Atoi(c.Params.ByName("number")) - - c.Writer.Header().Set("Content-Type", "text/event-stream") - - build, err := store.BuildNumber(repo, commitseq) - if err != nil { - c.Fail(404, err) - return - } - job, err := store.JobNumber(build, jobnum) - if err != nil { - c.Fail(404, err) - return - } - - rc, err := runner.Logs(job) - if err != nil { - c.Fail(404, err) - return - } - - defer func() { - rc.Close() - }() - - go func() { - <-c.Writer.CloseNotify() - rc.Close() - }() - - rw := &StreamWriter{c.Writer, 0} - - docker.StdCopy(rw, rw, rc) -} - -type StreamWriter struct { - writer gin.ResponseWriter - count int -} - -func (w *StreamWriter) Write(data []byte) (int, error) { - var err = sse.Encode(w.writer, sse.Event{ - Id: strconv.Itoa(w.count), - Event: "message", - Data: string(data), - }) - w.writer.Flush() - w.count += len(data) - return len(data), err -} diff --git a/drone.go b/drone.go new file mode 100644 index 0000000000..f9a2f9d2f2 --- /dev/null +++ b/drone.go @@ -0,0 +1,51 @@ +package main + +import ( + "flag" + + "github.com/drone/drone/engine" + "github.com/drone/drone/remote" + "github.com/drone/drone/router" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/shared/database" + "github.com/drone/drone/shared/envconfig" + "github.com/drone/drone/shared/server" + + "github.com/Sirupsen/logrus" +) + +var ( + dotenv = flag.String("config", ".env", "") + debug = flag.Bool("debug", true, "") +) + +func main() { + flag.Parse() + + // debug level if requested by user + if *debug { + logrus.SetLevel(logrus.DebugLevel) + } + + // Load the configuration from env file + env := envconfig.Load(*dotenv) + + // Setup the database driver + database_ := database.Load(env) + + // setup the remote driver + remote_ := remote.Load(env) + + // setup the runner + engine_ := engine.Load(database_, remote_) + + // setup the server and start the listener + server_ := server.Load(env) + server_.Run( + router.Load( + context.SetDatabase(database_), + context.SetRemote(remote_), + context.SetEngine(engine_), + ), + ) +} diff --git a/engine/bus.go b/engine/bus.go index c5e91d48b5..3ab1ece03f 100644 --- a/engine/bus.go +++ b/engine/bus.go @@ -1,28 +1,26 @@ -package builtin +package engine import ( "sync" - - "github.com/drone/drone/pkg/bus" ) -type Bus struct { +type eventbus struct { sync.Mutex - subs map[chan *bus.Event]bool + subs map[chan *Event]bool } -// New creates a new Bus that manages a list of +// New creates a new eventbus that manages a list of // subscribers to which events are published. -func New() *Bus { - return &Bus{ - subs: make(map[chan *bus.Event]bool), +func newEventbus() *eventbus { + return &eventbus{ + subs: make(map[chan *Event]bool), } } // Subscribe adds the channel to the list of // subscribers. Each subscriber in the list will // receive broadcast events. -func (b *Bus) Subscribe(c chan *bus.Event) { +func (b *eventbus) subscribe(c chan *Event) { b.Lock() b.subs[c] = true b.Unlock() @@ -30,19 +28,19 @@ func (b *Bus) Subscribe(c chan *bus.Event) { // Unsubscribe removes the channel from the // list of subscribers. -func (b *Bus) Unsubscribe(c chan *bus.Event) { +func (b *eventbus) unsubscribe(c chan *Event) { b.Lock() delete(b.subs, c) b.Unlock() } // Send dispatches a message to all subscribers. -func (b *Bus) Send(event *bus.Event) { +func (b *eventbus) send(event *Event) { b.Lock() defer b.Unlock() for s := range b.subs { - go func(c chan *bus.Event) { + go func(c chan *Event) { defer recover() c <- event }(s) diff --git a/engine/bus_test.go b/engine/bus_test.go index 6bb851421f..85b2e681e0 100644 --- a/engine/bus_test.go +++ b/engine/bus_test.go @@ -1,46 +1,45 @@ -package builtin +package engine import ( "testing" - . "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin" - "github.com/drone/drone/pkg/bus" + . "github.com/franela/goblin" ) -func TestBuild(t *testing.T) { +func TestBus(t *testing.T) { g := Goblin(t) - g.Describe("Bus", func() { + g.Describe("Event bus", func() { g.It("Should unsubscribe", func() { - c1 := make(chan *bus.Event) - c2 := make(chan *bus.Event) - b := New() - b.Subscribe(c1) - b.Subscribe(c2) + c1 := make(chan *Event) + c2 := make(chan *Event) + b := newEventbus() + b.subscribe(c1) + b.subscribe(c2) g.Assert(len(b.subs)).Equal(2) }) g.It("Should subscribe", func() { - c1 := make(chan *bus.Event) - c2 := make(chan *bus.Event) - b := New() - b.Subscribe(c1) - b.Subscribe(c2) + c1 := make(chan *Event) + c2 := make(chan *Event) + b := newEventbus() + b.subscribe(c1) + b.subscribe(c2) g.Assert(len(b.subs)).Equal(2) - b.Unsubscribe(c1) - b.Unsubscribe(c2) + b.unsubscribe(c1) + b.unsubscribe(c2) g.Assert(len(b.subs)).Equal(0) }) g.It("Should send", func() { em := map[string]bool{"foo": true, "bar": true} - e1 := &bus.Event{Name: "foo"} - e2 := &bus.Event{Name: "bar"} - c := make(chan *bus.Event) - b := New() - b.Subscribe(c) - b.Send(e1) - b.Send(e2) + e1 := &Event{Name: "foo"} + e2 := &Event{Name: "bar"} + c := make(chan *Event) + b := newEventbus() + b.subscribe(c) + b.send(e1) + b.send(e2) r1 := <-c r2 := <-c g.Assert(em[r1.Name]).Equal(true) diff --git a/engine/engine.go b/engine/engine.go new file mode 100644 index 0000000000..dbc95f3e76 --- /dev/null +++ b/engine/engine.go @@ -0,0 +1,392 @@ +package engine + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "database/sql" + "errors" + "fmt" + "io" + "io/ioutil" + "runtime" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/stdcopy" + "github.com/drone/drone/model" + "github.com/drone/drone/remote" + "github.com/drone/drone/shared/docker" + "github.com/samalba/dockerclient" +) + +type Engine interface { + Schedule(*Task) + Cancel(int64, int64, *model.Node) error + Stream(int64, int64, *model.Node) (io.ReadCloser, error) + Deallocate(*model.Node) + Allocate(*model.Node) bool + Subscribe(chan *Event) + Unsubscribe(chan *Event) +} + +var ( + // options to fetch the stdout and stderr logs + logOpts = &dockerclient.LogOptions{ + Stdout: true, + Stderr: true, + } + + // options to fetch the stdout and stderr logs + // by tailing the output. + logOptsTail = &dockerclient.LogOptions{ + Follow: true, + Stdout: true, + Stderr: true, + } + + // error when the system cannot find logs + errLogging = errors.New("Logs not available") +) + +type engine struct { + db *sql.DB + bus *eventbus + updater *updater + pool *pool +} + +// Load creates a new build engine, loaded with registered nodes from the +// database. The registered nodes are added to the pool of nodes to immediately +// start accepting workloads. +func Load(db *sql.DB, remote remote.Remote) Engine { + engine := &engine{} + engine.bus = newEventbus() + engine.pool = newPool() + engine.db = db + engine.updater = &updater{engine.bus, db, remote} + + nodes, err := model.GetNodeList(db) + if err != nil { + log.Fatalf("failed to get nodes from database. %s", err) + } + for _, node := range nodes { + engine.pool.allocate(node) + log.Infof("registered docker daemon %s", node.Addr) + } + + return engine +} + +// Cancel cancels the job running on the specified Node. +func (e *engine) Cancel(build, job int64, node *model.Node) error { + client, err := dockerclient.NewDockerClient(node.Addr, nil) + if err != nil { + return err + } + + id := fmt.Sprintf("drone_build_%d_job_%d", build, job) + return client.StopContainer(id, 30) +} + +// Stream streams the job output from the specified Node. +func (e *engine) Stream(build, job int64, node *model.Node) (io.ReadCloser, error) { + client, err := dockerclient.NewDockerClient(node.Addr, nil) + if err != nil { + log.Errorf("cannot create Docker client for node %s", node.Addr) + return nil, err + } + + id := fmt.Sprintf("drone_build_%d_job_%d", build, job) + log.Debugf("streaming container logs %s", id) + return client.ContainerLogs(id, logOptsTail) +} + +// Subscribe subscribes the channel to all build events. +func (e *engine) Subscribe(c chan *Event) { + e.bus.subscribe(c) +} + +// Unsubscribe unsubscribes the channel from all build events. +func (e *engine) Unsubscribe(c chan *Event) { + e.bus.unsubscribe(c) +} + +func (e *engine) Allocate(node *model.Node) bool { + log.Infof("registered docker daemon %s", node.Addr) + return e.pool.allocate(node) +} + +func (e *engine) Deallocate(n *model.Node) { + nodes := e.pool.list() + for _, node := range nodes { + if node.ID == n.ID { + log.Infof("un-registered docker daemon %s", node.Addr) + e.pool.deallocate(node) + break + } + } +} + +func (e *engine) Schedule(req *Task) { + node := <-e.pool.reserve() + + // since we are probably running in a go-routine + // make sure we recover from any panics so that + // a bug doesn't crash the whole system. + defer func() { + if err := recover(); err != nil { + + const size = 64 << 10 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + log.Printf("panic running build: %v\n%s", err, buf) + } + e.pool.release(node) + }() + + // update the node that was allocated to each job + func(id int64) { + tx, err := e.db.Begin() + if err != nil { + log.Errorf("error updating job to persist node. %s", err) + return + } + defer tx.Commit() + for _, job := range req.Jobs { + job.NodeID = id + model.UpdateJob(e.db, job) + } + }(node.ID) + + // run the full build! + client, err := newDockerClient(node.Addr, node.Cert, node.Key, node.CA) + if err != nil { + log.Errorln("error creating docker client", err) + } + + // update the build state if any of the sub-tasks + // had a non-success status + req.Build.Started = time.Now().UTC().Unix() + req.Build.Status = model.StatusRunning + e.updater.SetBuild(req) + + // run all bulid jobs + for _, job := range req.Jobs { + req.Job = job + runJob(req, e.updater, client) + } + + // TODO + req.Build.Status = model.StatusSuccess + for _, job := range req.Jobs { + if job.Status != model.StatusSuccess { + req.Build.Status = job.Status + break + } + } + req.Build.Finished = time.Now().UTC().Unix() + err = e.updater.SetBuild(req) + if err != nil { + log.Errorf("error updating build completion status. %s", err) + } + + // run notifications!!! + // for _ = range req.Jobs { + // err := runJobNotify(req, client) + // if err != nil { + // log.Errorf("error executing notification step. %s", err) + // } + // break + // } +} + +func newDockerClient(addr, cert, key, ca string) (dockerclient.Client, error) { + var tlc *tls.Config + + // create the Docket client TLS config + if len(cert) != 0 { + cert_, err := tls.LoadX509KeyPair(cert, key) + if err != nil { + return dockerclient.NewDockerClient(addr, nil) + } + + // create the TLS configuration for secure + // docker communications. + tlc = &tls.Config{ + Certificates: []tls.Certificate{cert_}, + } + + // use the certificate authority if provided. + // else don't use a certificate authority and set + // skip verify to true + if len(ca) != 0 { + pool := x509.NewCertPool() + pool.AppendCertsFromPEM([]byte(ca)) + tlc.RootCAs = pool + + } else { + tlc.InsecureSkipVerify = true + } + } + + // create the Docker client. In this version of Drone (alpha) + // we do not spread builds across clients, but this can and + // (probably) will change in the future. + return dockerclient.NewDockerClient(addr, tlc) +} + +func runJob(r *Task, updater *updater, client dockerclient.Client) error { + + name := fmt.Sprintf("drone_build_%d_job_%d", r.Build.ID, r.Job.ID) + + defer func() { + if r.Job.Status == model.StatusRunning { + r.Job.Status = model.StatusError + r.Job.Finished = time.Now().UTC().Unix() + r.Job.ExitCode = 255 + } + if r.Job.Status == model.StatusPending { + r.Job.Status = model.StatusError + r.Job.Started = time.Now().UTC().Unix() + r.Job.Finished = time.Now().UTC().Unix() + r.Job.ExitCode = 255 + } + updater.SetJob(r) + + client.KillContainer(name, "9") + client.RemoveContainer(name, true, true) + }() + + // marks the task as running + r.Job.Status = model.StatusRunning + r.Job.Started = time.Now().UTC().Unix() + + // encode the build payload to write to stdin + // when launching the build container + in, err := encodeToLegacyFormat(r) + if err != nil { + log.Errorf("failure to marshal work. %s", err) + return err + } + + // CREATE AND START BUILD + args := DefaultBuildArgs + if r.Build.Event == model.EventPull { + args = DefaultPullRequestArgs + } + args = append(args, "--") + args = append(args, string(in)) + + conf := &dockerclient.ContainerConfig{ + Image: DefaultAgent, + Entrypoint: DefaultEntrypoint, + Cmd: args, + HostConfig: dockerclient.HostConfig{ + Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"}, + }, + Volumes: map[string]struct{}{ + "/var/run/docker.sock": struct{}{}, + }, + } + + // w.client.PullImage(conf.Image, nil) + + _, err = docker.RunDaemon(client, conf, name) + if err != nil { + log.Errorf("error starting build container. %s", err) + return err + } + + // UPDATE STATUS + + err = updater.SetJob(r) + if err != nil { + log.Errorf("error updating job status as running. %s", err) + return err + } + + // WAIT FOR OUTPUT + info, builderr := docker.Wait(client, name) + + switch { + case info.State.ExitCode == 128: + r.Job.ExitCode = info.State.ExitCode + r.Job.Status = model.StatusKilled + case info.State.ExitCode == 130: + r.Job.ExitCode = info.State.ExitCode + r.Job.Status = model.StatusKilled + case builderr != nil: + r.Job.Status = model.StatusError + case info.State.ExitCode != 0: + r.Job.ExitCode = info.State.ExitCode + r.Job.Status = model.StatusFailure + default: + r.Job.Status = model.StatusSuccess + } + + // send the logs to the datastore + var buf bytes.Buffer + rc, err := client.ContainerLogs(name, docker.LogOpts) + if err != nil && builderr != nil { + buf.WriteString("Error launching build") + buf.WriteString(builderr.Error()) + } else if err != nil { + buf.WriteString("Error launching build") + buf.WriteString(err.Error()) + log.Errorf("error opening connection to logs. %s", err) + return err + } else { + defer rc.Close() + stdcopy.StdCopy(&buf, &buf, io.LimitReader(rc, 5000000)) + } + + // update the task in the datastore + r.Job.Finished = time.Now().UTC().Unix() + err = updater.SetJob(r) + if err != nil { + log.Errorf("error updating job after completion. %s", err) + return err + } + + err = updater.SetLogs(r, ioutil.NopCloser(&buf)) + if err != nil { + log.Errorf("error updating logs. %s", err) + return err + } + + log.Debugf("completed job %d with status %s.", r.Job.ID, r.Job.Status) + return nil +} + +func runJobNotify(r *Task, client dockerclient.Client) error { + + name := fmt.Sprintf("drone_build_%d_notify", r.Build.ID) + + defer func() { + client.KillContainer(name, "9") + client.RemoveContainer(name, true, true) + }() + + // encode the build payload to write to stdin + // when launching the build container + in, err := encodeToLegacyFormat(r) + if err != nil { + log.Errorf("failure to marshal work. %s", err) + return err + } + + args := DefaultNotifyArgs + args = append(args, "--") + args = append(args, string(in)) + + conf := &dockerclient.ContainerConfig{ + Image: DefaultAgent, + Entrypoint: DefaultEntrypoint, + Cmd: args, + HostConfig: dockerclient.HostConfig{}, + } + + _, err = docker.Run(client, conf, name) + return err +} diff --git a/engine/pool.go b/engine/pool.go new file mode 100644 index 0000000000..a886b47dea --- /dev/null +++ b/engine/pool.go @@ -0,0 +1,86 @@ +package engine + +import ( + "sync" + + "github.com/drone/drone/model" +) + +type pool struct { + sync.Mutex + nodes map[*model.Node]bool + nodec chan *model.Node +} + +func newPool() *pool { + return &pool{ + nodes: make(map[*model.Node]bool), + nodec: make(chan *model.Node, 999), + } +} + +// Allocate allocates a node to the pool to +// be available to accept work. +func (p *pool) allocate(n *model.Node) bool { + if p.isAllocated(n) { + return false + } + + p.Lock() + p.nodes[n] = true + p.Unlock() + + p.nodec <- n + return true +} + +// IsAllocated is a helper function that returns +// true if the node is currently allocated to +// the pool. +func (p *pool) isAllocated(n *model.Node) bool { + p.Lock() + defer p.Unlock() + _, ok := p.nodes[n] + return ok +} + +// Deallocate removes the node from the pool of +// available nodes. If the node is currently +// reserved and performing work it will finish, +// but no longer be given new work. +func (p *pool) deallocate(n *model.Node) { + p.Lock() + defer p.Unlock() + delete(p.nodes, n) +} + +// List returns a list of all model.Nodes currently +// allocated to the pool. +func (p *pool) list() []*model.Node { + p.Lock() + defer p.Unlock() + + var nodes []*model.Node + for n := range p.nodes { + nodes = append(nodes, n) + } + return nodes +} + +// Reserve reserves the next available node to +// start doing work. Once work is complete, the +// node should be released back to the pool. +func (p *pool) reserve() <-chan *model.Node { + return p.nodec +} + +// Release releases the node back to the pool +// of available nodes. +func (p *pool) release(n *model.Node) bool { + if !p.isAllocated(n) { + return false + } + + p.nodec <- n + return true +} diff --git a/engine/pool_test.go b/engine/pool_test.go new file mode 100644 index 0000000000..852847396e --- /dev/null +++ b/engine/pool_test.go @@ -0,0 +1,89 @@ +package engine + +import ( + "testing" + + "github.com/drone/drone/model" + "github.com/franela/goblin" +) + +func TestPool(t *testing.T) { + + g := goblin.Goblin(t) + g.Describe("Pool", func() { + + g.It("Should allocate nodes", func() { + n := &model.Node{Addr: "unix:///var/run/docker.sock"} + pool := newPool() + pool.allocate(n) + g.Assert(len(pool.nodes)).Equal(1) + g.Assert(len(pool.nodec)).Equal(1) + g.Assert(pool.nodes[n]).Equal(true) + }) + + g.It("Should not re-allocate an allocated node", func() { + n := &model.Node{Addr: "unix:///var/run/docker.sock"} + pool := newPool() + g.Assert(pool.allocate(n)).Equal(true) + g.Assert(pool.allocate(n)).Equal(false) + }) + + g.It("Should reserve a node", func() { + n := &model.Node{Addr: "unix:///var/run/docker.sock"} + pool := newPool() + pool.allocate(n) + g.Assert(<-pool.reserve()).Equal(n) + }) + + g.It("Should release a node", func() { + n := &model.Node{Addr: "unix:///var/run/docker.sock"} + pool := newPool() + pool.allocate(n) + g.Assert(len(pool.nodec)).Equal(1) + g.Assert(<-pool.reserve()).Equal(n) + g.Assert(len(pool.nodec)).Equal(0) + pool.release(n) + g.Assert(len(pool.nodec)).Equal(1) + g.Assert(<-pool.reserve()).Equal(n) + g.Assert(len(pool.nodec)).Equal(0) + }) + + g.It("Should not release an unallocated node", func() { + n := &model.Node{Addr: "unix:///var/run/docker.sock"} + pool := newPool() + g.Assert(len(pool.nodes)).Equal(0) + g.Assert(len(pool.nodec)).Equal(0) + pool.release(n) + g.Assert(len(pool.nodes)).Equal(0) + g.Assert(len(pool.nodec)).Equal(0) + pool.release(nil) + g.Assert(len(pool.nodes)).Equal(0) + g.Assert(len(pool.nodec)).Equal(0) + }) + + g.It("Should list all allocated nodes", func() { + n1 := &model.Node{Addr: "unix:///var/run/docker.sock"} + n2 := &model.Node{Addr: "unix:///var/run/docker.sock"} + pool := newPool() + pool.allocate(n1) + pool.allocate(n2) + g.Assert(len(pool.nodes)).Equal(2) + g.Assert(len(pool.nodec)).Equal(2) + g.Assert(len(pool.list())).Equal(2) + }) + + g.It("Should remove a node", func() { + n1 := &model.Node{Addr: "unix:///var/run/docker.sock"} + n2 := &model.Node{Addr: "unix:///var/run/docker.sock"} + pool := newPool() + pool.allocate(n1) + pool.allocate(n2) + g.Assert(len(pool.nodes)).Equal(2) + pool.deallocate(n1) + pool.deallocate(n2) + g.Assert(len(pool.nodes)).Equal(0) + g.Assert(len(pool.list())).Equal(0) + }) + + }) +} diff --git a/engine/queue.go b/engine/queue.go deleted file mode 100644 index 3b39a6e83c..0000000000 --- a/engine/queue.go +++ /dev/null @@ -1,123 +0,0 @@ -package builtin - -import ( - "errors" - "sync" - - "github.com/drone/drone/pkg/queue" -) - -var ErrNotFound = errors.New("work item not found") - -type Queue struct { - sync.Mutex - - acks map[*queue.Work]struct{} - items map[*queue.Work]struct{} - itemc chan *queue.Work -} - -func New() *Queue { - return &Queue{ - acks: make(map[*queue.Work]struct{}), - items: make(map[*queue.Work]struct{}), - itemc: make(chan *queue.Work, 999), - } -} - -// Publish inserts work at the tail of this queue, waiting for -// space to become available if the queue is full. -func (q *Queue) Publish(work *queue.Work) error { - q.Lock() - q.items[work] = struct{}{} - q.Unlock() - q.itemc <- work - return nil -} - -// Remove removes the specified work item from this queue, -// if it is present. -func (q *Queue) Remove(work *queue.Work) error { - q.Lock() - defer q.Unlock() - - _, ok := q.items[work] - if !ok { - return ErrNotFound - } - var items []*queue.Work - - // loop through and drain all items - // from the queue. -drain: - for { - select { - case item := <-q.itemc: - items = append(items, item) - default: - break drain - } - } - - // re-add all items to the queue except - // the item we're trying to remove - for _, item := range items { - if item == work { - delete(q.items, work) - delete(q.acks, work) - continue - } - q.itemc <- item - } - return nil -} - -// Pull retrieves and removes the head of this queue, waiting -// if necessary until work becomes available. -func (q *Queue) Pull() *queue.Work { - work := <-q.itemc - q.Lock() - delete(q.items, work) - q.acks[work] = struct{}{} - q.Unlock() - return work -} - -// PullClose retrieves and removes the head of this queue, -// waiting if necessary until work becomes available. The -// CloseNotifier should be provided to clone the channel -// if the subscribing client terminates its connection. -func (q *Queue) PullClose(cn queue.CloseNotifier) *queue.Work { - for { - select { - case <-cn.CloseNotify(): - return nil - case work := <-q.itemc: - q.Lock() - delete(q.items, work) - q.acks[work] = struct{}{} - q.Unlock() - return work - } - } -} - -// Ack acknowledges an item in the queue was processed. -func (q *Queue) Ack(work *queue.Work) error { - q.Lock() - delete(q.acks, work) - q.Unlock() - return nil -} - -// Items returns a slice containing all of the work in this -// queue, in proper sequence. -func (q *Queue) Items() []*queue.Work { - q.Lock() - defer q.Unlock() - items := []*queue.Work{} - for work := range q.items { - items = append(items, work) - } - return items -} diff --git a/engine/queue_test.go b/engine/queue_test.go deleted file mode 100644 index dcdf46ebda..0000000000 --- a/engine/queue_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package builtin - -import ( - "sync" - "testing" - - . "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin" - "github.com/drone/drone/pkg/queue" -) - -func TestBuild(t *testing.T) { - g := Goblin(t) - g.Describe("Queue", func() { - - g.It("Should publish item", func() { - w1 := &queue.Work{} - w2 := &queue.Work{} - q := New() - q.Publish(w1) - q.Publish(w2) - g.Assert(len(q.items)).Equal(2) - g.Assert(len(q.itemc)).Equal(2) - }) - - g.It("Should remove item", func() { - w1 := &queue.Work{} - w2 := &queue.Work{} - w3 := &queue.Work{} - q := New() - q.Publish(w1) - q.Publish(w2) - q.Publish(w3) - q.Remove(w2) - g.Assert(len(q.items)).Equal(2) - g.Assert(len(q.itemc)).Equal(2) - g.Assert(q.Pull()).Equal(w1) - g.Assert(q.Pull()).Equal(w3) - g.Assert(q.Remove(w2)).Equal(ErrNotFound) - }) - - g.It("Should pull item", func() { - w1 := &queue.Work{} - w2 := &queue.Work{} - q := New() - c := new(closeNotifier) - q.Publish(w1) - q.Publish(w2) - g.Assert(q.Pull()).Equal(w1) - g.Assert(q.PullClose(c)).Equal(w2) - g.Assert(q.acks[w1]).Equal(struct{}{}) - g.Assert(q.acks[w2]).Equal(struct{}{}) - g.Assert(len(q.acks)).Equal(2) - }) - - g.It("Should cancel pulling item", func() { - q := New() - c := new(closeNotifier) - c.closec = make(chan bool, 1) - var wg sync.WaitGroup - go func() { - wg.Add(1) - g.Assert(q.PullClose(c) == nil).IsTrue() - wg.Done() - }() - go func() { - c.closec <- true - }() - wg.Wait() - }) - - g.It("Should ack item", func() { - w := &queue.Work{} - c := new(closeNotifier) - q := New() - q.Publish(w) - g.Assert(q.PullClose(c)).Equal(w) - g.Assert(len(q.acks)).Equal(1) - g.Assert(q.Ack(w)).Equal(nil) - g.Assert(len(q.acks)).Equal(0) - }) - - g.It("Should get all items", func() { - q := New() - q.Publish(&queue.Work{}) - q.Publish(&queue.Work{}) - q.Publish(&queue.Work{}) - g.Assert(len(q.Items())).Equal(3) - }) - }) -} - -type closeNotifier struct { - closec chan bool -} - -func (c *closeNotifier) CloseNotify() <-chan bool { - return c.closec -} diff --git a/engine/runner.go b/engine/runner.go deleted file mode 100644 index df3c719670..0000000000 --- a/engine/runner.go +++ /dev/null @@ -1,318 +0,0 @@ -package builtin - -import ( - "bytes" - "crypto/tls" - "crypto/x509" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "os" - "time" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/samalba/dockerclient" - "github.com/drone/drone/pkg/docker" - "github.com/drone/drone/pkg/queue" - "github.com/drone/drone/pkg/types" - - log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus" -) - -var ( - // Defult docker host address - DefaultHost = "unix:///var/run/docker.sock" - - // Docker host address from environment variable - DockerHost = os.Getenv("DOCKER_HOST") - - // Docker TLS variables - DockerHostCa = os.Getenv("DOCKER_CA") - DockerHostKey = os.Getenv("DOCKER_KEY") - DockerHostCert = os.Getenv("DOCKER_CERT") -) - -func init() { - // if the environment doesn't specify a DOCKER_HOST - // we should use the default Docker socket. - if len(DockerHost) == 0 { - DockerHost = DefaultHost - } -} - -type Runner struct { - Updater -} - -func newDockerClient() (dockerclient.Client, error) { - var tlc *tls.Config - - // create the Docket client TLS config - if len(DockerHostCert) > 0 && len(DockerHostKey) > 0 && len(DockerHostCa) > 0 { - cert, err := tls.LoadX509KeyPair(DockerHostCert, DockerHostKey) - if err != nil { - log.Errorf("failure to load SSL cert and key. %s", err) - return dockerclient.NewDockerClient(DockerHost, nil) - } - caCert, err := ioutil.ReadFile(DockerHostCa) - if err != nil { - log.Errorf("failure to load SSL CA cert. %s", err) - return dockerclient.NewDockerClient(DockerHost, nil) - } - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(caCert) - tlc = &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: caCertPool, - } - } - - // create the Docker client. In this version of Drone (alpha) - // we do not spread builds across clients, but this can and - // (probably) will change in the future. - return dockerclient.NewDockerClient(DockerHost, tlc) -} - -func (r *Runner) Run(w *queue.Work) error { - var workers []*worker - var client dockerclient.Client - - defer func() { - recover() - - // ensures that all containers have been removed - // from the host machine. - for _, worker := range workers { - worker.Remove() - } - - // if any part of the commit fails and leaves - // behind orphan sub-builds we need to cleanup - // after ourselves. - if w.Build.Status == types.StateRunning { - // if any tasks are running or pending - // we should mark them as complete. - for _, b := range w.Build.Jobs { - if b.Status == types.StateRunning { - b.Status = types.StateError - b.Finished = time.Now().UTC().Unix() - b.ExitCode = 255 - } - if b.Status == types.StatePending { - b.Status = types.StateError - b.Started = time.Now().UTC().Unix() - b.Finished = time.Now().UTC().Unix() - b.ExitCode = 255 - } - r.SetJob(w.Repo, w.Build, b) - } - // must populate build start - if w.Build.Started == 0 { - w.Build.Started = time.Now().UTC().Unix() - } - // mark the build as complete (with error) - w.Build.Status = types.StateError - w.Build.Finished = time.Now().UTC().Unix() - r.SetBuild(w.User, w.Repo, w.Build) - } - }() - - // marks the build as running - w.Build.Started = time.Now().UTC().Unix() - w.Build.Status = types.StateRunning - err := r.SetBuild(w.User, w.Repo, w.Build) - if err != nil { - log.Errorf("failure to set build. %s", err) - return err - } - - // create the Docker client. In this version of Drone (alpha) - // we do not spread builds across clients, but this can and - // (probably) will change in the future. - client, err = newDockerClient() - if err != nil { - log.Errorf("failure to connect to docker. %s", err) - return err - } - - // loop through and execute the build and - // clone steps for each build job. - for _, job := range w.Build.Jobs { - - // marks the task as running - job.Status = types.StateRunning - job.Started = time.Now().UTC().Unix() - err = r.SetJob(w.Repo, w.Build, job) - if err != nil { - log.Errorf("failure to set job. %s", err) - return err - } - - work := &work{ - System: w.System, - Workspace: &types.Workspace{Netrc: w.Netrc, Keys: w.Keys}, - Repo: w.Repo, - Build: w.Build, - Job: job, - Secret: string(w.Secret), - Config: string(w.Config), - } - in, err := json.Marshal(work) - if err != nil { - log.Errorf("failure to marshalise work. %s", err) - return err - } - - worker := newWorker(client) - workers = append(workers, worker) - cname := cname(job) - pullrequest := (w.Build.PullRequest != nil && w.Build.PullRequest.Number != 0) - state, builderr := worker.Build(cname, in, pullrequest) - - switch { - case state == 128: - job.ExitCode = state - job.Status = types.StateKilled - case state == 130: - job.ExitCode = state - job.Status = types.StateKilled - case builderr != nil: - job.Status = types.StateError - case state != 0: - job.ExitCode = state - job.Status = types.StateFailure - default: - job.Status = types.StateSuccess - } - - // send the logs to the datastore - var buf bytes.Buffer - rc, err := worker.Logs() - if err != nil && builderr != nil { - buf.WriteString("001 Error launching build") - buf.WriteString(builderr.Error()) - } else if err != nil { - buf.WriteString("002 Error launching build") - buf.WriteString(err.Error()) - return err - } else { - defer rc.Close() - docker.StdCopy(&buf, &buf, rc) - } - err = r.SetLogs(w.Repo, w.Build, job, ioutil.NopCloser(&buf)) - if err != nil { - return err - } - - // update the task in the datastore - job.Finished = time.Now().UTC().Unix() - err = r.SetJob(w.Repo, w.Build, job) - if err != nil { - return err - } - } - - // update the build state if any of the sub-tasks - // had a non-success status - w.Build.Status = types.StateSuccess - for _, job := range w.Build.Jobs { - if job.Status != types.StateSuccess { - w.Build.Status = job.Status - break - } - } - err = r.SetBuild(w.User, w.Repo, w.Build) - if err != nil { - return err - } - - // loop through and execute the notifications and - // the destroy all containers afterward. - for i, job := range w.Build.Jobs { - work := &work{ - System: w.System, - Workspace: &types.Workspace{Netrc: w.Netrc, Keys: w.Keys}, - Repo: w.Repo, - Build: w.Build, - Job: job, - Secret: string(w.Secret), - Config: string(w.Config), - } - in, err := json.Marshal(work) - if err != nil { - return err - } - workers[i].Notify(in) - break - } - - return nil -} - -func (r *Runner) Cancel(job *types.Job) error { - client, err := newDockerClient() - if err != nil { - return err - } - return client.StopContainer(cname(job), 30) -} - -func (r *Runner) Logs(job *types.Job) (io.ReadCloser, error) { - client, err := newDockerClient() - if err != nil { - return nil, err - } - // make sure this container actually exists - info, err := client.InspectContainer(cname(job)) - if err != nil { - - // add a small exponential backoff since there - // is a small window when the container hasn't - // been created yet, but the build is about to start - for i := 0; ; i++ { - time.Sleep(1 * time.Second) - info, err = client.InspectContainer(cname(job)) - if err != nil && i == 5 { - return nil, err - } - if err == nil { - break - } - } - } - - // verify the container is running. if not we'll - // do an exponential backoff and attempt to wait - if !info.State.Running { - for i := 0; ; i++ { - time.Sleep(1 * time.Second) - info, err = client.InspectContainer(info.Id) - if err != nil { - return nil, err - } - if info.State.Running { - break - } - if i == 5 { - return nil, dockerclient.ErrNotFound - } - } - } - - return client.ContainerLogs(info.Id, logOptsTail) -} - -func cname(job *types.Job) string { - return fmt.Sprintf("drone-%d", job.ID) -} - -func (r *Runner) Poll(q queue.Queue) { - for { - w := q.Pull() - q.Ack(w) - err := r.Run(w) - if err != nil { - log.Error(err) - } - } -} diff --git a/engine/types.go b/engine/types.go new file mode 100644 index 0000000000..27cf6ec752 --- /dev/null +++ b/engine/types.go @@ -0,0 +1,24 @@ +package engine + +import ( + "github.com/drone/drone/model" +) + +type Event struct { + Name string + Msg []byte +} + +type Task struct { + User *model.User `json:"-"` + Repo *model.Repo `json:"repo"` + Build *model.Build `json:"build"` + BuildPrev *model.Build `json:"build_last"` + Jobs []*model.Job `json:"jobs"` + Job *model.Job `json:"job"` + Keys *model.Key `json:"keys"` + Netrc *model.Netrc `json:"netrc"` + Config string `json:"config"` + Secret string `json:"secret"` + System *model.System `json:"system"` +} diff --git a/engine/updater.go b/engine/updater.go index 634743c57f..c36af905ff 100644 --- a/engine/updater.go +++ b/engine/updater.go @@ -1,94 +1,67 @@ -package builtin +package engine import ( + "database/sql" "encoding/json" "fmt" "io" - "github.com/drone/drone/pkg/bus" - "github.com/drone/drone/pkg/remote" - "github.com/drone/drone/pkg/store" - "github.com/drone/drone/pkg/types" + "github.com/drone/drone/model" + "github.com/drone/drone/remote" ) -type Updater interface { - SetBuild(*types.User, *types.Repo, *types.Build) error - SetJob(*types.Repo, *types.Build, *types.Job) error - SetLogs(*types.Repo, *types.Build, *types.Job, io.ReadCloser) error -} - -// NewUpdater returns an implementation of the Updater interface -// that directly modifies the database and sends messages to the bus. -func NewUpdater(bus bus.Bus, store store.Store, rem remote.Remote) Updater { - return &updater{bus, store, rem} -} - type updater struct { - bus bus.Bus - store store.Store + bus *eventbus + db *sql.DB remote remote.Remote } -func (u *updater) SetBuild(user *types.User, r *types.Repo, c *types.Build) error { - err := u.store.SetBuild(c) +func (u *updater) SetBuild(r *Task) error { + err := model.UpdateBuild(u.db, r.Build) if err != nil { return err } - err = u.remote.Status(user, r, c) + err = u.remote.Status(r.User, r.Repo, r.Build, fmt.Sprintf("%s/%s/%d", r.System.Link, r.Repo.FullName, r.Build.Number)) if err != nil { // log err } - // we need this because builds coming from - // a remote agent won't have the embedded - // build list. we should probably just rethink - // the messaging instead of this hack. - if c.Jobs == nil || len(c.Jobs) == 0 { - c.Jobs, _ = u.store.JobList(c) - } - - msg, err := json.Marshal(c) + msg, err := json.Marshal(&payload{r.Build, r.Jobs}) if err != nil { return err } - u.bus.Send(&bus.Event{ - Name: r.FullName, - Kind: bus.EventRepo, + u.bus.send(&Event{ + Name: r.Repo.FullName, Msg: msg, }) return nil } -func (u *updater) SetJob(r *types.Repo, c *types.Build, j *types.Job) error { - err := u.store.SetJob(j) +func (u *updater) SetJob(r *Task) error { + err := model.UpdateJob(u.db, r.Job) if err != nil { return err } - // we need this because builds coming from - // a remote agent won't have the embedded - // build list. we should probably just rethink - // the messaging instead of this hack. - if c.Jobs == nil || len(c.Jobs) == 0 { - c.Jobs, _ = u.store.JobList(c) - } - - msg, err := json.Marshal(c) + msg, err := json.Marshal(&payload{r.Build, r.Jobs}) if err != nil { return err } - u.bus.Send(&bus.Event{ - Name: r.FullName, - Kind: bus.EventRepo, + u.bus.send(&Event{ + Name: r.Repo.FullName, Msg: msg, }) return nil } -func (u *updater) SetLogs(r *types.Repo, c *types.Build, j *types.Job, rc io.ReadCloser) error { - path := fmt.Sprintf("/logs/%s/%v/%v", r.FullName, c.Number, j.Number) - return u.store.SetBlobReader(path, rc) +func (u *updater) SetLogs(r *Task, rc io.ReadCloser) error { + return model.SetLog(u.db, r.Job, rc) +} + +type payload struct { + *model.Build + Jobs []*model.Job `json:"jobs"` } diff --git a/engine/util.go b/engine/util.go new file mode 100644 index 0000000000..db195754ad --- /dev/null +++ b/engine/util.go @@ -0,0 +1,35 @@ +package engine + +import ( + "encoding/json" +) + +func encodeToLegacyFormat(t *Task) ([]byte, error) { + t.System.Plugins = append(t.System.Plugins, "plugins/*") + + s := map[string]interface{}{} + s["repo"] = t.Repo + s["config"] = t.Config + s["secret"] = t.Secret + s["job"] = t.Job + s["system"] = t.System + s["workspace"] = map[string]interface{}{ + "netrc": t.Netrc, + "keys": t.Keys, + } + s["build"] = map[string]interface{}{ + "number": t.Build.Number, + "status": t.Build.Status, + "head_commit": map[string]interface{}{ + "sha": t.Build.Commit, + "ref": t.Build.Ref, + "branch": t.Build.Branch, + "message": t.Build.Message, + "author": map[string]interface{}{ + "login": t.Build.Author, + "email": t.Build.Email, + }, + }, + } + return json.Marshal(&s) +} diff --git a/engine/worker.go b/engine/worker.go index 3e5270828b..43d427ab00 100644 --- a/engine/worker.go +++ b/engine/worker.go @@ -1,30 +1,10 @@ -package builtin +package engine import ( - "errors" "io" - "io/ioutil" - "github.com/drone/drone/Godeps/_workspace/src/github.com/samalba/dockerclient" - "github.com/drone/drone/pkg/types" -) - -var ErrLogging = errors.New("Logs not available") - -var ( - // options to fetch the stdout and stderr logs - logOpts = &dockerclient.LogOptions{ - Stdout: true, - Stderr: true, - } - - // options to fetch the stdout and stderr logs - // by tailing the output. - logOptsTail = &dockerclient.LogOptions{ - Follow: true, - Stdout: true, - Stderr: true, - } + "github.com/drone/drone/shared/docker" + "github.com/samalba/dockerclient" ) var ( @@ -35,26 +15,15 @@ var ( DefaultEntrypoint = []string{"/bin/drone-exec"} // default argument to invoke build steps - DefaultBuildArgs = []string{"--pull", "--cache", "--clone", "--build", "--deploy"} + DefaultBuildArgs = []string{"--cache", "--debug", "--clone", "--build", "--deploy"} // default argument to invoke build steps - DefaultPullRequestArgs = []string{"--pull", "--cache", "--clone", "--build"} + DefaultPullRequestArgs = []string{"--cache", "--clone", "--build"} // default arguments to invoke notify steps - DefaultNotifyArgs = []string{"--pull", "--notify"} + DefaultNotifyArgs = []string{"--notify"} ) -type work struct { - Repo *types.Repo `json:"repo"` - Build *types.Build `json:"build"` - BuildLast *types.Build `json:"build_last"` - Job *types.Job `json:"job"` - System *types.System `json:"system"` - Workspace *types.Workspace `json:"workspace"` - Secret string `json:"secret"` - Config string `json:"config"` -} - type worker struct { client dockerclient.Client build *dockerclient.ContainerInfo @@ -84,7 +53,6 @@ func (w *worker) Build(name string, stdin []byte, pr bool) (_ int, err error) { Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"}, }, Volumes: map[string]struct{}{ - "/drone": struct{}{}, "/var/run/docker.sock": struct{}{}, }, } @@ -92,9 +60,9 @@ func (w *worker) Build(name string, stdin []byte, pr bool) (_ int, err error) { // TEMPORARY: always try to pull the new image for now // since we'll be frequently updating the build image // for the next few weeks - w.client.PullImage(conf.Image, nil) + // w.client.PullImage(conf.Image, nil) - w.build, err = run(w.client, conf, name) + w.build, err = docker.Run(w.client, conf, name) if err != nil { return 1, err } @@ -103,16 +71,7 @@ func (w *worker) Build(name string, stdin []byte, pr bool) (_ int, err error) { // Notify executes the notification steps. func (w *worker) Notify(stdin []byte) error { - // use the affinity parameter in case we are - // using Docker swarm as a backend. - environment := []string{"affinity:container==" + w.build.Id} - - // the build container is acting as an ambassador container - // with a shared filesystem . - volume := []string{w.build.Id} - // the command line arguments passed into the - // build agent container. args := DefaultNotifyArgs args = append(args, "--") args = append(args, string(stdin)) @@ -121,14 +80,11 @@ func (w *worker) Notify(stdin []byte) error { Image: DefaultAgent, Entrypoint: DefaultEntrypoint, Cmd: args, - Env: environment, - HostConfig: dockerclient.HostConfig{ - VolumesFrom: volume, - }, + HostConfig: dockerclient.HostConfig{}, } var err error - w.notify, err = run(w.client, conf, "") + w.notify, err = docker.Run(w.client, conf, "") return err } @@ -136,7 +92,7 @@ func (w *worker) Notify(stdin []byte) error { // from the build and deploy agents. func (w *worker) Logs() (io.ReadCloser, error) { if w.build == nil { - return nil, ErrLogging + return nil, errLogging } return w.client.ContainerLogs(w.build.Id, logOpts) } @@ -153,77 +109,3 @@ func (w *worker) Remove() { w.client.RemoveContainer(w.build.Id, true, true) } } - -// run is a helper function that creates and starts a container, -// blocking until either complete. -func run(client dockerclient.Client, conf *dockerclient.ContainerConfig, name string) (*dockerclient.ContainerInfo, error) { - - // attempts to create the contianer - id, err := client.CreateContainer(conf, name) - if err != nil { - // and pull the image and re-create if that fails - client.PullImage(conf.Image, nil) - id, err = client.CreateContainer(conf, name) - // make sure the container is removed in - // the event of a creation error. - if err != nil && len(id) != 0 { - client.RemoveContainer(id, true, true) - } - if err != nil { - return nil, err - } - } - - // ensures the container is always stopped - // and ready to be removed. - defer func() { - client.StopContainer(id, 5) - client.KillContainer(id, "9") - }() - - // fetches the container information. - info, err := client.InspectContainer(id) - if err != nil { - return nil, err - } - - // channel listening for errors while the - // container is running async. - errc := make(chan error, 1) - infoc := make(chan *dockerclient.ContainerInfo, 1) - go func() { - - // starts the container - err := client.StartContainer(id, &conf.HostConfig) - if err != nil { - errc <- err - return - } - - // blocks and waits for the container to finish - // by streaming the logs (to /dev/null). Ideally - // we could use the `wait` function instead - rc, err := client.ContainerLogs(id, logOptsTail) - if err != nil { - errc <- err - return - } - io.Copy(ioutil.Discard, rc) - rc.Close() - - // fetches the container information - info, err := client.InspectContainer(id) - if err != nil { - errc <- err - return - } - infoc <- info - }() - - select { - case info := <-infoc: - return info, nil - case err := <-errc: - return info, err - } -} diff --git a/make.go b/make.go deleted file mode 100644 index 04b932d368..0000000000 --- a/make.go +++ /dev/null @@ -1,426 +0,0 @@ -// +build ignore - -// This program builds Drone. -// $ go run make.go deps bindata build test - -package main - -import ( - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" -) - -var ( - version = "0.4" - sha = rev() -) - -// list of all posible steps that can be executed -// as part of the build process. -var steps = map[string]step{ - "deps": executeDeps, - "json": executeJson, - "embed": executeEmbed, - "scripts": executeScripts, - "styles": executeStyles, - "vet": executeVet, - "fmt": executeFmt, - "test": executeTest, - "build": executeBuild, - "install": executeInstall, - "image": executeImage, - "bindata": executeBindata, - "clean": executeClean, -} - -func main() { - for _, arg := range os.Args[1:] { - step, ok := steps[arg] - - if !ok { - fmt.Println("Error: Invalid step", arg) - os.Exit(1) - } - - err := step() - - if err != nil { - fmt.Println("Error: Failed step", arg) - os.Exit(1) - } - } -} - -type step func() error - -func executeDeps() error { - deps := []string{ - "github.com/jteeuwen/go-bindata/...", - "golang.org/x/tools/cmd/cover", - } - - for _, dep := range deps { - err := run( - "go", - "get", - "-u", - dep) - - if err != nil { - return err - } - } - - return nil -} - -// json step generates optimized json marshal and -// unmarshal functions to override defaults. -func executeJson() error { - return nil -} - -// embed step embeds static files in .go files. -func executeEmbed() error { - // embed drone.{revision}.css - // embed drone.{revision}.js - - return nil -} - -// scripts step concatinates all javascript files. -func executeScripts() error { - files := []string{ - "cmd/drone-server/static/scripts/term.js", - "cmd/drone-server/static/scripts/drone.js", - "cmd/drone-server/static/scripts/controllers/repos.js", - "cmd/drone-server/static/scripts/controllers/builds.js", - "cmd/drone-server/static/scripts/controllers/users.js", - "cmd/drone-server/static/scripts/services/repos.js", - "cmd/drone-server/static/scripts/services/builds.js", - "cmd/drone-server/static/scripts/services/users.js", - "cmd/drone-server/static/scripts/services/logs.js", - "cmd/drone-server/static/scripts/services/tokens.js", - "cmd/drone-server/static/scripts/services/feed.js", - "cmd/drone-server/static/scripts/filters/filter.js", - "cmd/drone-server/static/scripts/filters/gravatar.js", - "cmd/drone-server/static/scripts/filters/time.js", - } - - f, err := os.OpenFile( - "cmd/drone-server/static/scripts/drone.min.js", - os.O_CREATE|os.O_RDWR|os.O_TRUNC, - 0660) - - defer f.Close() - - if err != nil { - fmt.Println("Failed to open output file") - return err - } - - for _, input := range files { - content, err := ioutil.ReadFile(input) - - if err != nil { - return err - } - - f.Write(content) - } - - return nil -} - -// styles step concatinates the stylesheet files. -func executeStyles() error { - files := []string{ - "cmd/drone-server/static/styles/reset.css", - "cmd/drone-server/static/styles/fonts.css", - "cmd/drone-server/static/styles/alert.css", - "cmd/drone-server/static/styles/blankslate.css", - "cmd/drone-server/static/styles/list.css", - "cmd/drone-server/static/styles/label.css", - "cmd/drone-server/static/styles/range.css", - "cmd/drone-server/static/styles/switch.css", - "cmd/drone-server/static/styles/main.css", - } - - f, err := os.OpenFile( - "cmd/drone-server/static/styles/drone.min.css", - os.O_CREATE|os.O_RDWR|os.O_TRUNC, - 0660) - - defer f.Close() - - if err != nil { - fmt.Println("Failed to open output file") - return err - } - - for _, input := range files { - content, err := ioutil.ReadFile(input) - - if err != nil { - return err - } - - f.Write(content) - } - - return nil -} - -// vet step executes the `go vet` command -func executeVet() error { - return run( - "go", - "vet", - "github.com/drone/drone/pkg/...", - "github.com/drone/drone/cmd/...") -} - -// fmt step executes the `go fmt` command -func executeFmt() error { - return run( - "go", - "fmt", - "github.com/drone/drone/pkg/...", - "github.com/drone/drone/cmd/...") -} - -// test step executes unit tests and coverage. -func executeTest() error { - ldf := fmt.Sprintf( - "-X main.revision=%s -X main.version=%s", - sha, - version) - - return run( - "go", - "test", - "-cover", - "-ldflags", - ldf, - "github.com/drone/drone/pkg/...", - "github.com/drone/drone/cmd/...") -} - -// install step installs the application binaries. -func executeInstall() error { - var bins = []struct { - input string - }{ - { - "github.com/drone/drone/cmd/drone-server", - }, - } - - for _, bin := range bins { - ldf := fmt.Sprintf( - "-X main.revision=%s -X main.version=%s", - sha, - version) - - err := run( - "go", - "install", - "-ldflags", - ldf, - bin.input) - - if err != nil { - return err - } - } - - return nil -} - -// build step creates the application binaries. -func executeBuild() error { - var bins = []struct { - input string - output string - }{ - { - "github.com/drone/drone/cmd/drone-server", - "bin/drone", - }, - } - - for _, bin := range bins { - ldf := fmt.Sprintf( - "-X main.revision=%s -X main.version=%s", - sha, - version) - - err := run( - "go", - "build", - "-o", - bin.output, - "-ldflags", - ldf, - bin.input) - - if err != nil { - return err - } - } - - return nil -} - -// image step builds docker images. -func executeImage() error { - var images = []struct { - dir string - name string - }{ - { - "bin/drone-server", - "drone/drone", - }, - } - for _, image := range images { - path := filepath.Join( - image.dir, - "Dockerfile") - - name := fmt.Sprintf("%s:%s", - image.name, - version) - - err := run( - "docker", - "build", - "-rm", - path, - name) - - if err != nil { - return err - } - } - - return nil -} - -// bindata step generates go-bindata package. -func executeBindata() error { - var paths = []struct { - input string - output string - pkg string - }{ - { - "cmd/drone-server/static/...", - "cmd/drone-server/drone_bindata.go", - "main", - }, - } - - for _, path := range paths { - binErr := run( - "go-bindata", - fmt.Sprintf("-o=%s", path.output), - fmt.Sprintf("-pkg=%s", path.pkg), - path.input) - - if binErr != nil { - return binErr - } - - fmtErr := run( - "go", - "fmt", - path.output) - - if fmtErr != nil { - return fmtErr - } - } - - return nil -} - -// clean step removes all generated files. -func executeClean() error { - err := filepath.Walk(".", func(path string, f os.FileInfo, err error) error { - suffixes := []string{ - ".out", - "_bindata.go", - } - - for _, suffix := range suffixes { - if strings.HasSuffix(path, suffix) { - if err := os.Remove(path); err != nil { - return err - } - } - } - - return nil - }) - - if err != nil { - return err - } - - files := []string{ - "bin/drone", - } - - for _, file := range files { - if _, err := os.Stat(file); err != nil { - continue - } - - if err := os.Remove(file); err != nil { - return err - } - } - - return nil -} - -// run is a helper function that executes commands -// and assigns stdout and stderr targets -func run(command string, args ...string) error { - cmd := exec.Command(command, args...) - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - trace(cmd.Args) - return cmd.Run() -} - -// helper function to parse the git revision -func rev() string { - cmd := exec.Command( - "git", - "rev-parse", - "--short", - "HEAD") - - raw, err := cmd.CombinedOutput() - - if err != nil { - return "HEAD" - } - - return strings.Trim(string(raw), "\n") -} - -// trace is a helper function that writes a command -// to stdout similar to bash +x -func trace(args []string) { - print("+ ") - println(strings.Join(args, " ")) -} diff --git a/model/build.go b/model/build.go index 415ff7e3dd..1562b8694a 100644 --- a/model/build.go +++ b/model/build.go @@ -1,48 +1,146 @@ -package types - -const ( - StatePending = "pending" - StateRunning = "running" - StateSuccess = "success" - StateFailure = "failure" - StateKilled = "killed" - StateError = "error" +package model + +import ( + "time" + + "github.com/drone/drone/shared/database" + "github.com/russross/meddler" ) type Build struct { - ID int64 `json:"id"` - RepoID int64 `json:"-" sql:"unique:ux_build_number,index:ix_build_repo_id"` - Number int `json:"number" sql:"unique:ux_build_number"` - Event string `json:"event"` - Status string `json:"status"` - Started int64 `json:"started_at"` - Finished int64 `json:"finished_at"` + ID int64 `json:"id" meddler:"build_id,pk"` + RepoID int64 `json:"-" meddler:"build_repo_id"` + Number int `json:"number" meddler:"build_number"` + Event string `json:"event" meddler:"build_event"` + Status string `json:"status" meddler:"build_status"` + Created int64 `json:"created_at" meddler:"build_created"` + Started int64 `json:"started_at" meddler:"build_started"` + Finished int64 `json:"finished_at" meddler:"build_finished"` + Commit string `json:"commit" meddler:"build_commit"` + Branch string `json:"branch" meddler:"build_branch"` + Ref string `json:"ref" meddler:"build_ref"` + Refspec string `json:"refspec" meddler:"build_refspec"` + Remote string `json:"remote" meddler:"build_remote"` + Title string `json:"title" meddler:"build_title"` + Message string `json:"message" meddler:"build_message"` + Timestamp string `json:"timestamp" meddler:"build_timestamp"` + Author string `json:"author" meddler:"build_author"` + Avatar string `json:"author_avatar" meddler:"build_avatar"` + Email string `json:"author_email" meddler:"build_email"` + Link string `json:"link_url" meddler:"build_link"` +} + +type BuildGroup struct { + Date string + Builds []*Build +} - Commit *Commit `json:"head_commit"` - PullRequest *PullRequest `json:"pull_request,omitempty"` +func GetBuild(db meddler.DB, id int64) (*Build, error) { + var build = new(Build) + var err = meddler.Load(db, buildTable, build, id) + return build, err +} + +func GetBuildNumber(db meddler.DB, repo *Repo, number int) (*Build, error) { + var build = new(Build) + var err = meddler.QueryRow(db, build, database.Rebind(buildNumberQuery), repo.ID, number) + return build, err +} + +func GetBuildRef(db meddler.DB, repo *Repo, ref string) (*Build, error) { + var build = new(Build) + var err = meddler.QueryRow(db, build, database.Rebind(buildRefQuery), repo.ID, ref) + return build, err +} - Jobs []*Job `json:"jobs,omitempty" sql:"-"` +func GetBuildCommit(db meddler.DB, repo *Repo, sha, branch string) (*Build, error) { + var build = new(Build) + var err = meddler.QueryRow(db, build, database.Rebind(buildCommitQuery), repo.ID, sha, branch) + return build, err } -type PullRequest struct { - Number int `json:"number,omitempty"` - Title string `json:"title,omitempty"` - Link string `json:"link_url,omitempty"` - Base *Commit `json:"base_commit,omitempty"` +func GetBuildLast(db meddler.DB, repo *Repo, branch string) (*Build, error) { + var build = new(Build) + var err = meddler.QueryRow(db, build, database.Rebind(buildLastQuery), repo.ID, branch) + return build, err } -type Commit struct { - Sha string `json:"sha"` - Ref string `json:"ref"` - Link string `json:"link_url,omitempty"` - Branch string `json:"branch" sql:"index:ix_commit_branch"` - Message string `json:"message"` - Timestamp string `json:"timestamp,omitempty"` - Remote string `json:"remote,omitempty"` - Author *Author `json:"author,omitempty"` +func GetBuildList(db meddler.DB, repo *Repo) ([]*Build, error) { + var builds = []*Build{} + var err = meddler.QueryAll(db, &builds, database.Rebind(buildListQuery), repo.ID) + return builds, err } -type Author struct { - Login string `json:"login,omitempty"` - Email string `json:"email,omitempty"` +func CreateBuild(db meddler.DB, build *Build, jobs ...*Job) error { + var number int + db.QueryRow(buildNumberLast, build.RepoID).Scan(&number) + build.Number = number + 1 + build.Created = time.Now().UTC().Unix() + err := meddler.Insert(db, buildTable, build) + if err != nil { + return err + } + for i, job := range jobs { + job.BuildID = build.ID + job.Number = i + 1 + err = InsertJob(db, job) + if err != nil { + return err + } + } + return nil } + +func UpdateBuild(db meddler.DB, build *Build) error { + return meddler.Update(db, buildTable, build) +} + +const buildTable = "builds" + +const buildListQuery = ` +SELECT * +FROM builds +WHERE build_repo_id = ? +ORDER BY build_number DESC +LIMIT 50 +` + +const buildNumberQuery = ` +SELECT * +FROM builds +WHERE build_repo_id = ? + AND build_number = ? +LIMIT 1; +` + +const buildLastQuery = ` +SELECT * +FROM builds +WHERE build_repo_id = ? + AND build_branch = ? +ORDER BY build_number DESC +LIMIT 1 +` + +const buildCommitQuery = ` +SELECT * +FROM builds +WHERE build_repo_id = ? + AND build_commit = ? + AND build_branch = ? +LIMIT 1 +` + +const buildRefQuery = ` +SELECT * +FROM builds +WHERE build_repo_id = ? + AND build_ref = ? +LIMIT 1 +` + +const buildNumberLast = ` +SELECT MAX(build_number) +FROM builds +WHERE build_repo_id = ? +` diff --git a/model/build_test.go b/model/build_test.go new file mode 100644 index 0000000000..38bb9104d5 --- /dev/null +++ b/model/build_test.go @@ -0,0 +1,207 @@ +package model + +import ( + "testing" + + "github.com/drone/drone/shared/database" + "github.com/franela/goblin" +) + +func TestBuild(t *testing.T) { + db := database.Open("sqlite3", ":memory:") + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Builds", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM builds") + db.Exec("DELETE FROM jobs") + }) + + g.It("Should Post a Build", func() { + build := Build{ + RepoID: 1, + Status: StatusSuccess, + Commit: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + err := CreateBuild(db, &build, []*Job{}...) + g.Assert(err == nil).IsTrue() + g.Assert(build.ID != 0).IsTrue() + g.Assert(build.Number).Equal(1) + g.Assert(build.Commit).Equal("85f8c029b902ed9400bc600bac301a0aadb144ac") + }) + + g.It("Should Put a Build", func() { + build := Build{ + RepoID: 1, + Number: 5, + Status: StatusSuccess, + Commit: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + CreateBuild(db, &build, []*Job{}...) + build.Status = StatusRunning + err1 := UpdateBuild(db, &build) + getbuild, err2 := GetBuild(db, build.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(build.ID).Equal(getbuild.ID) + g.Assert(build.RepoID).Equal(getbuild.RepoID) + g.Assert(build.Status).Equal(getbuild.Status) + g.Assert(build.Number).Equal(getbuild.Number) + }) + + g.It("Should Get a Build", func() { + build := Build{ + RepoID: 1, + Status: StatusSuccess, + } + CreateBuild(db, &build, []*Job{}...) + getbuild, err := GetBuild(db, build.ID) + g.Assert(err == nil).IsTrue() + g.Assert(build.ID).Equal(getbuild.ID) + g.Assert(build.RepoID).Equal(getbuild.RepoID) + g.Assert(build.Status).Equal(getbuild.Status) + }) + + g.It("Should Get a Build by Number", func() { + build1 := &Build{ + RepoID: 1, + Status: StatusPending, + } + build2 := &Build{ + RepoID: 1, + Status: StatusPending, + } + err1 := CreateBuild(db, build1, []*Job{}...) + err2 := CreateBuild(db, build2, []*Job{}...) + getbuild, err3 := GetBuildNumber(db, &Repo{ID: 1}, build2.Number) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(build2.ID).Equal(getbuild.ID) + g.Assert(build2.RepoID).Equal(getbuild.RepoID) + g.Assert(build2.Number).Equal(getbuild.Number) + }) + + g.It("Should Get a Build by Ref", func() { + build1 := &Build{ + RepoID: 1, + Status: StatusPending, + Ref: "refs/pull/5", + } + build2 := &Build{ + RepoID: 1, + Status: StatusPending, + Ref: "refs/pull/6", + } + err1 := CreateBuild(db, build1, []*Job{}...) + err2 := CreateBuild(db, build2, []*Job{}...) + getbuild, err3 := GetBuildRef(db, &Repo{ID: 1}, "refs/pull/6") + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(build2.ID).Equal(getbuild.ID) + g.Assert(build2.RepoID).Equal(getbuild.RepoID) + g.Assert(build2.Number).Equal(getbuild.Number) + g.Assert(build2.Ref).Equal(getbuild.Ref) + }) + + g.It("Should Get a Build by Ref", func() { + build1 := &Build{ + RepoID: 1, + Status: StatusPending, + Ref: "refs/pull/5", + } + build2 := &Build{ + RepoID: 1, + Status: StatusPending, + Ref: "refs/pull/6", + } + err1 := CreateBuild(db, build1, []*Job{}...) + err2 := CreateBuild(db, build2, []*Job{}...) + getbuild, err3 := GetBuildRef(db, &Repo{ID: 1}, "refs/pull/6") + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(build2.ID).Equal(getbuild.ID) + g.Assert(build2.RepoID).Equal(getbuild.RepoID) + g.Assert(build2.Number).Equal(getbuild.Number) + g.Assert(build2.Ref).Equal(getbuild.Ref) + }) + + g.It("Should Get a Build by Commit", func() { + build1 := &Build{ + RepoID: 1, + Status: StatusPending, + Branch: "master", + Commit: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + build2 := &Build{ + RepoID: 1, + Status: StatusPending, + Branch: "dev", + Commit: "85f8c029b902ed9400bc600bac301a0aadb144aa", + } + err1 := CreateBuild(db, build1, []*Job{}...) + err2 := CreateBuild(db, build2, []*Job{}...) + getbuild, err3 := GetBuildCommit(db, &Repo{ID: 1}, build2.Commit, build2.Branch) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(build2.ID).Equal(getbuild.ID) + g.Assert(build2.RepoID).Equal(getbuild.RepoID) + g.Assert(build2.Number).Equal(getbuild.Number) + g.Assert(build2.Commit).Equal(getbuild.Commit) + g.Assert(build2.Branch).Equal(getbuild.Branch) + }) + + g.It("Should Get a Build by Commit", func() { + build1 := &Build{ + RepoID: 1, + Status: StatusFailure, + Branch: "master", + Commit: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + build2 := &Build{ + RepoID: 1, + Status: StatusSuccess, + Branch: "master", + Commit: "85f8c029b902ed9400bc600bac301a0aadb144aa", + } + err1 := CreateBuild(db, build1, []*Job{}...) + err2 := CreateBuild(db, build2, []*Job{}...) + getbuild, err3 := GetBuildLast(db, &Repo{ID: 1}, build2.Branch) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(build2.ID).Equal(getbuild.ID) + g.Assert(build2.RepoID).Equal(getbuild.RepoID) + g.Assert(build2.Number).Equal(getbuild.Number) + g.Assert(build2.Status).Equal(getbuild.Status) + g.Assert(build2.Branch).Equal(getbuild.Branch) + g.Assert(build2.Commit).Equal(getbuild.Commit) + }) + + g.It("Should get recent Builds", func() { + build1 := &Build{ + RepoID: 1, + Status: StatusFailure, + } + build2 := &Build{ + RepoID: 1, + Status: StatusSuccess, + } + CreateBuild(db, build1, []*Job{}...) + CreateBuild(db, build2, []*Job{}...) + builds, err := GetBuildList(db, &Repo{ID: 1}) + g.Assert(err == nil).IsTrue() + g.Assert(len(builds)).Equal(2) + g.Assert(builds[0].ID).Equal(build2.ID) + g.Assert(builds[0].RepoID).Equal(build2.RepoID) + g.Assert(builds[0].Status).Equal(build2.Status) + }) + }) +} diff --git a/shared/ccmenu/ccmenu.go b/model/cc.go similarity index 70% rename from shared/ccmenu/ccmenu.go rename to model/cc.go index 8c4bf3f27b..590beb753a 100644 --- a/shared/ccmenu/ccmenu.go +++ b/model/cc.go @@ -1,11 +1,9 @@ -package ccmenu +package model import ( "encoding/xml" "strconv" "time" - - "github.com/drone/drone/pkg/types" ) type CCProjects struct { @@ -23,10 +21,10 @@ type CCProject struct { WebURL string `xml:"webUrl,attr"` } -func NewCC(r *types.Repo, b *types.Build) *CCProjects { +func NewCC(r *Repo, b *Build, link string) *CCProjects { proj := &CCProject{ - Name: r.Owner + "/" + r.Name, - WebURL: r.Self, + Name: r.FullName, + WebURL: link, Activity: "Building", LastBuildStatus: "Unknown", LastBuildLabel: "Unknown", @@ -34,24 +32,22 @@ func NewCC(r *types.Repo, b *types.Build) *CCProjects { // if the build is not currently running then // we can return the latest build status. - if b.Status != types.StatePending && - b.Status != types.StateRunning { + if b.Status != StatusPending && + b.Status != StatusRunning { proj.Activity = "Sleeping" proj.LastBuildTime = time.Unix(b.Started, 0).Format(time.RFC3339) proj.LastBuildLabel = strconv.Itoa(b.Number) } - // ensure the last build state accepts a valid + // ensure the last build Status accepts a valid // ccmenu enumeration switch b.Status { - case types.StateError, types.StateKilled: + case StatusError, StatusKilled: proj.LastBuildStatus = "Exception" - case types.StateSuccess: + case StatusSuccess: proj.LastBuildStatus = "Success" - case types.StateFailure: + case StatusFailure: proj.LastBuildStatus = "Failure" - default: - proj.LastBuildStatus = "Unknown" } return &CCProjects{Project: proj} diff --git a/model/cc_test.go b/model/cc_test.go new file mode 100644 index 0000000000..2420715eda --- /dev/null +++ b/model/cc_test.go @@ -0,0 +1,83 @@ +package model + +import ( + "testing" + + "github.com/franela/goblin" +) + +func TestCC(t *testing.T) { + + g := goblin.Goblin(t) + g.Describe("CC", func() { + + g.It("Should create a project", func() { + + r := &Repo{ + FullName: "foo/bar", + } + b := &Build{ + Status: StatusSuccess, + Number: 1, + Started: 1442872675, + } + cc := NewCC(r, b, "http://localhost/foo/bar/1") + + g.Assert(cc.Project.Name).Equal("foo/bar") + g.Assert(cc.Project.Activity).Equal("Sleeping") + g.Assert(cc.Project.LastBuildStatus).Equal("Success") + g.Assert(cc.Project.LastBuildLabel).Equal("1") + g.Assert(cc.Project.LastBuildTime).Equal("2015-09-21T14:57:55-07:00") + g.Assert(cc.Project.WebURL).Equal("http://localhost/foo/bar/1") + }) + + g.It("Should properly label exceptions", func() { + r := &Repo{FullName: "foo/bar"} + b := &Build{ + Status: StatusError, + Number: 1, + Started: 1257894000, + } + cc := NewCC(r, b, "http://localhost/foo/bar/1") + g.Assert(cc.Project.LastBuildStatus).Equal("Exception") + g.Assert(cc.Project.Activity).Equal("Sleeping") + }) + + g.It("Should properly label success", func() { + r := &Repo{FullName: "foo/bar"} + b := &Build{ + Status: StatusSuccess, + Number: 1, + Started: 1257894000, + } + cc := NewCC(r, b, "http://localhost/foo/bar/1") + g.Assert(cc.Project.LastBuildStatus).Equal("Success") + g.Assert(cc.Project.Activity).Equal("Sleeping") + }) + + g.It("Should properly label failure", func() { + r := &Repo{FullName: "foo/bar"} + b := &Build{ + Status: StatusFailure, + Number: 1, + Started: 1257894000, + } + cc := NewCC(r, b, "http://localhost/foo/bar/1") + g.Assert(cc.Project.LastBuildStatus).Equal("Failure") + g.Assert(cc.Project.Activity).Equal("Sleeping") + }) + + g.It("Should properly label running", func() { + r := &Repo{FullName: "foo/bar"} + b := &Build{ + Status: StatusRunning, + Number: 1, + Started: 1257894000, + } + cc := NewCC(r, b, "http://localhost/foo/bar/1") + g.Assert(cc.Project.Activity).Equal("Building") + g.Assert(cc.Project.LastBuildStatus).Equal("Unknown") + g.Assert(cc.Project.LastBuildLabel).Equal("Unknown") + }) + }) +} diff --git a/model/config.go b/model/config.go deleted file mode 100644 index 0c8fbd5575..0000000000 --- a/model/config.go +++ /dev/null @@ -1,126 +0,0 @@ -package types - -import ( - "path/filepath" - "strings" -) - -// Config represents a repository build configuration. -type Config struct { - Cache *Step - Setup *Step - Clone *Step - Build *Step - - Compose map[string]*Step - Publish map[string]*Step - Deploy map[string]*Step - Notify map[string]*Step - - Matrix Matrix - Axis Axis -} - -// Matrix represents the build matrix. -type Matrix map[string][]string - -// Axis represents a single permutation of entries -// from the build matrix. -type Axis map[string]string - -// String returns a string representation of an Axis as -// a comma-separated list of environment variables. -func (a Axis) String() string { - var envs []string - for k, v := range a { - envs = append(envs, k+"="+v) - } - return strings.Join(envs, " ") -} - -// Step represents a step in the build process, including -// the execution environment and parameters. -type Step struct { - Image string - Pull bool - Privileged bool - Environment []string - Entrypoint []string - Command []string - Volumes []string - Cache []string - WorkingDir string `yaml:"working_dir"` - NetworkMode string `yaml:"net"` - - // Condition represents a set of conditions that must - // be met in order to execute this step. - Condition *Condition `yaml:"when"` - - // Config represents the unique configuration details - // for each plugin. - Config map[string]interface{} `yaml:"config,inline"` -} - -// Condition represents a set of conditions that must -// be met in order to proceed with a build or build step. -type Condition struct { - Owner string // Indicates the step should run only for this repo (useful for forks) - Branch string // Indicates the step should run only for this branch - Event string - Success string - Failure string - - // Indicates the step should only run when the following - // matrix values are present for the sub-build. - Matrix map[string]string -} - -// MatchBranch is a helper function that returns true -// if all_branches is true. Else it returns false if a -// branch condition is specified, and the branch does -// not match. -func (c *Condition) MatchBranch(branch string) bool { - if len(c.Branch) == 0 { - return true - } - if strings.HasPrefix(branch, "refs/heads/") { - branch = branch[11:] - } - match, _ := filepath.Match(c.Branch, branch) - return match -} - -// MatchOwner is a helper function that returns false -// if an owner condition is specified and the repository -// owner does not match. -// -// This is useful when you want to prevent forks from -// executing deployment, publish or notification steps. -func (c *Condition) MatchOwner(owner string) bool { - if len(c.Owner) == 0 { - return true - } - parts := strings.Split(owner, "/") - switch len(parts) { - case 2: - return c.Owner == parts[0] - case 3: - return c.Owner == parts[1] - default: - return c.Owner == owner - } -} - -// MatchMatrix is a helper function that returns false -// to limit steps to only certain matrix axis. -func (c *Condition) MatchMatrix(matrix map[string]string) bool { - if len(c.Matrix) == 0 { - return true - } - for k, v := range c.Matrix { - if matrix[k] != v { - return false - } - } - return true -} diff --git a/model/const.go b/model/const.go new file mode 100644 index 0000000000..96b2db30f4 --- /dev/null +++ b/model/const.go @@ -0,0 +1,18 @@ +package model + +const ( + EventPush = "push" + EventPull = "pull_request" + EventTag = "tag" + EventDeploy = "deploy" +) + +const ( + StatusSkipped = "skipped" + StatusPending = "pending" + StatusRunning = "running" + StatusSuccess = "success" + StatusFailure = "failure" + StatusKilled = "killed" + StatusError = "error" +) diff --git a/model/feed.go b/model/feed.go new file mode 100644 index 0000000000..c690ad9e39 --- /dev/null +++ b/model/feed.go @@ -0,0 +1,23 @@ +package model + +type Feed struct { + Owner string `json:"owner" meddler:"repo_owner"` + Name string `json:"name" meddler:"repo_name"` + FullName string `json:"full_name" meddler:"repo_full_name"` + Avatar string `json:"avatar_url" meddler:"repo_avatar"` + + Number int `json:"number" meddler:"build_number"` + Event string `json:"event" meddler:"build_event"` + Status string `json:"status" meddler:"build_status"` + Started int64 `json:"started_at" meddler:"build_started"` + Finished int64 `json:"finished_at" meddler:"build_finished"` + Commit string `json:"commit" meddler:"build_commit"` + Branch string `json:"branch" meddler:"build_branch"` + Ref string `json:"ref" meddler:"build_ref"` + Refspec string `json:"refspec" meddler:"build_refspec"` + Remote string `json:"remote" meddler:"build_remote"` + Title string `json:"title" meddler:"build_title"` + Message string `json:"message" meddler:"build_message"` + Author string `json:"author" meddler:"build_author"` + Email string `json:"author_email" meddler:"build_email"` +} diff --git a/model/hook.go b/model/hook.go deleted file mode 100644 index 12678bd21e..0000000000 --- a/model/hook.go +++ /dev/null @@ -1,8 +0,0 @@ -package types - -type Hook struct { - Event string - Repo *Repo - Commit *Commit - PullRequest *PullRequest -} diff --git a/model/job.go b/model/job.go index 2cc242b321..6f61d8a879 100644 --- a/model/job.go +++ b/model/job.go @@ -1,13 +1,62 @@ -package types +package model + +import ( + "github.com/drone/drone/shared/database" + "github.com/russross/meddler" +) type Job struct { - ID int64 `json:"id"` - BuildID int64 `json:"-" sql:"unique:ux_build_number,index:ix_job_build_id"` - Number int `json:"number" sql:"unique:ux_build_number"` - Status string `json:"status"` - ExitCode int `json:"exit_code"` - Started int64 `json:"started_at"` - Finished int64 `json:"finished_at"` + ID int64 `json:"id" meddler:"job_id,pk"` + BuildID int64 `json:"-" meddler:"job_build_id"` + NodeID int64 `json:"-" meddler:"job_node_id"` + Number int `json:"number" meddler:"job_number"` + Status string `json:"status" meddler:"job_status"` + ExitCode int `json:"exit_code" meddler:"job_exit_code"` + Started int64 `json:"started_at" meddler:"job_started"` + Finished int64 `json:"finished_at" meddler:"job_finished"` + + Environment map[string]string `json:"environment" meddler:"job_environment,json"` +} + +func GetJob(db meddler.DB, id int64) (*Job, error) { + var job = new(Job) + var err = meddler.Load(db, jobTable, job, id) + return job, err +} + +func GetJobNumber(db meddler.DB, build *Build, number int) (*Job, error) { + var job = new(Job) + var err = meddler.QueryRow(db, job, database.Rebind(jobNumberQuery), build.ID, number) + return job, err +} - Environment map[string]string `json:"environment" sql:"type:varchar,size:2048"` +func GetJobList(db meddler.DB, build *Build) ([]*Job, error) { + var jobs = []*Job{} + var err = meddler.QueryAll(db, &jobs, database.Rebind(jobListQuery), build.ID) + return jobs, err } + +func InsertJob(db meddler.DB, job *Job) error { + return meddler.Insert(db, jobTable, job) +} + +func UpdateJob(db meddler.DB, job *Job) error { + return meddler.Update(db, jobTable, job) +} + +const jobTable = "jobs" + +const jobListQuery = ` +SELECT * +FROM jobs +WHERE job_build_id = ? +ORDER BY job_number ASC +` + +const jobNumberQuery = ` +SELECT * +FROM jobs +WHERE job_build_id = ? +AND job_number = ? +LIMIT 1 +` diff --git a/model/job_test.go b/model/job_test.go new file mode 100644 index 0000000000..2291760374 --- /dev/null +++ b/model/job_test.go @@ -0,0 +1,117 @@ +package model + +import ( + "testing" + + "github.com/drone/drone/shared/database" + "github.com/franela/goblin" +) + +func TestJob(t *testing.T) { + db := database.Open("sqlite3", ":memory:") + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Job", func() { + + // before each test we purge the package table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM jobs") + db.Exec("DELETE FROM builds") + }) + + g.It("Should Set a job", func() { + job := &Job{ + BuildID: 1, + Status: "pending", + ExitCode: 0, + Number: 1, + } + err1 := InsertJob(db, job) + g.Assert(err1 == nil).IsTrue() + g.Assert(job.ID != 0).IsTrue() + + job.Status = "started" + err2 := UpdateJob(db, job) + g.Assert(err2 == nil).IsTrue() + + getjob, err3 := GetJob(db, job.ID) + g.Assert(err3 == nil).IsTrue() + g.Assert(getjob.Status).Equal(job.Status) + }) + + g.It("Should Get a Job by ID", func() { + job := &Job{ + BuildID: 1, + Status: "pending", + ExitCode: 1, + Number: 1, + Environment: map[string]string{"foo": "bar"}, + } + err1 := InsertJob(db, job) + g.Assert(err1 == nil).IsTrue() + g.Assert(job.ID != 0).IsTrue() + + getjob, err2 := GetJob(db, job.ID) + g.Assert(err2 == nil).IsTrue() + g.Assert(getjob.ID).Equal(job.ID) + g.Assert(getjob.Status).Equal(job.Status) + g.Assert(getjob.ExitCode).Equal(job.ExitCode) + g.Assert(getjob.Environment).Equal(job.Environment) + g.Assert(getjob.Environment["foo"]).Equal("bar") + }) + + g.It("Should Get a Job by Number", func() { + job := &Job{ + BuildID: 1, + Status: "pending", + ExitCode: 1, + Number: 1, + } + err1 := InsertJob(db, job) + g.Assert(err1 == nil).IsTrue() + g.Assert(job.ID != 0).IsTrue() + + getjob, err2 := GetJobNumber(db, &Build{ID: 1}, 1) + g.Assert(err2 == nil).IsTrue() + g.Assert(getjob.ID).Equal(job.ID) + g.Assert(getjob.Status).Equal(job.Status) + }) + + g.It("Should Get a List of Jobs by Commit", func() { + + build := Build{ + RepoID: 1, + Status: StatusSuccess, + } + jobs := []*Job{ + &Job{ + BuildID: 1, + Status: "success", + ExitCode: 0, + Number: 1, + }, + &Job{ + BuildID: 3, + Status: "error", + ExitCode: 1, + Number: 2, + }, + &Job{ + BuildID: 5, + Status: "pending", + ExitCode: 0, + Number: 3, + }, + } + // + err1 := CreateBuild(db, &build, jobs...) + g.Assert(err1 == nil).IsTrue() + getjobs, err2 := GetJobList(db, &build) + g.Assert(err2 == nil).IsTrue() + g.Assert(len(getjobs)).Equal(3) + g.Assert(getjobs[0].Number).Equal(1) + g.Assert(getjobs[0].Status).Equal(StatusSuccess) + }) + }) +} diff --git a/model/key.go b/model/key.go new file mode 100644 index 0000000000..a7a39850ba --- /dev/null +++ b/model/key.go @@ -0,0 +1,46 @@ +package model + +import ( + "github.com/drone/drone/shared/database" + "github.com/russross/meddler" +) + +type Key struct { + ID int64 `json:"-" meddler:"key_id,pk"` + RepoID int64 `json:"-" meddler:"key_repo_id"` + Public string `json:"public" meddler:"key_public"` + Private string `json:"private" meddler:"key_private"` +} + +func GetKey(db meddler.DB, repo *Repo) (*Key, error) { + var key = new(Key) + var err = meddler.QueryRow(db, key, database.Rebind(keyQuery), repo.ID) + return key, err +} + +func CreateKey(db meddler.DB, key *Key) error { + return meddler.Save(db, keyTable, key) +} + +func UpdateKey(db meddler.DB, key *Key) error { + return meddler.Save(db, keyTable, key) +} + +func DeleteKey(db meddler.DB, repo *Repo) error { + var _, err = db.Exec(database.Rebind(keyDeleteStmt), repo.ID) + return err +} + +const keyTable = "keys" + +const keyQuery = ` +SELECT * +FROM keys +WHERE key_repo_id=? +LIMIT 1 +` + +const keyDeleteStmt = ` +DELETE FROM keys +WHERE key_repo_id=? +` diff --git a/model/key_test.go b/model/key_test.go new file mode 100644 index 0000000000..398e9f6092 --- /dev/null +++ b/model/key_test.go @@ -0,0 +1,113 @@ +package model + +import ( + "testing" + + "github.com/drone/drone/shared/database" + "github.com/franela/goblin" +) + +func TestKey(t *testing.T) { + db := database.Open("sqlite3", ":memory:") + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Keys", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM keys") + }) + + g.It("Should create a key", func() { + key := Key{ + RepoID: 1, + Public: fakePublicKey, + Private: fakePrivateKey, + } + err := CreateKey(db, &key) + g.Assert(err == nil).IsTrue() + g.Assert(key.ID != 0).IsTrue() + }) + + g.It("Should update a key", func() { + key := Key{ + RepoID: 1, + Public: fakePublicKey, + Private: fakePrivateKey, + } + err := CreateKey(db, &key) + g.Assert(err == nil).IsTrue() + g.Assert(key.ID != 0).IsTrue() + + key.Private = "" + key.Public = "" + + err1 := UpdateKey(db, &key) + getkey, err2 := GetKey(db, &Repo{ID: 1}) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(key.ID).Equal(getkey.ID) + g.Assert(key.Public).Equal(getkey.Public) + g.Assert(key.Private).Equal(getkey.Private) + }) + + g.It("Should get a key", func() { + key := Key{ + RepoID: 1, + Public: fakePublicKey, + Private: fakePrivateKey, + } + err := CreateKey(db, &key) + g.Assert(err == nil).IsTrue() + g.Assert(key.ID != 0).IsTrue() + + getkey, err := GetKey(db, &Repo{ID: 1}) + g.Assert(err == nil).IsTrue() + g.Assert(key.ID).Equal(getkey.ID) + g.Assert(key.Public).Equal(getkey.Public) + g.Assert(key.Private).Equal(getkey.Private) + }) + + g.It("Should delete a key", func() { + key := Key{ + RepoID: 1, + Public: fakePublicKey, + Private: fakePrivateKey, + } + err1 := CreateKey(db, &key) + err2 := DeleteKey(db, &Repo{ID: 1}) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + + _, err := GetKey(db, &Repo{ID: 1}) + g.Assert(err == nil).IsFalse() + }) + }) +} + +var fakePublicKey = ` +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0 +FPqri0cb2JZfXJ/DgYSF6vUpwmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/ +3j+skZ6UtW+5u09lHNsj6tQ51s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQAB +-----END PUBLIC KEY----- +` + +var fakePrivateKey = ` + +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp +wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5 +1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh +3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2 +pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX +GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il +AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF +L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k +X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl +U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ +37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0= +-----END RSA PRIVATE KEY----- +` diff --git a/model/log.go b/model/log.go new file mode 100644 index 0000000000..fe107cbdd7 --- /dev/null +++ b/model/log.go @@ -0,0 +1,42 @@ +package model + +import ( + "bytes" + "io" + "io/ioutil" + + "github.com/drone/drone/shared/database" + "github.com/russross/meddler" +) + +type Log struct { + ID int64 `meddler:"log_id,pk"` + JobID int64 `meddler:"log_job_id"` + Data []byte `meddler:"log_data"` +} + +func GetLog(db meddler.DB, job *Job) (io.ReadCloser, error) { + var log = new(Log) + var err = meddler.QueryRow(db, log, database.Rebind(logQuery), job.ID) + var buf = bytes.NewBuffer(log.Data) + return ioutil.NopCloser(buf), err +} + +func SetLog(db meddler.DB, job *Job, r io.Reader) error { + var log = new(Log) + var err = meddler.QueryRow(db, log, database.Rebind(logQuery), job.ID) + if err != nil { + log = &Log{JobID: job.ID} + } + log.Data, _ = ioutil.ReadAll(r) + return meddler.Save(db, logTable, log) +} + +const logTable = "logs" + +const logQuery = ` +SELECT * +FROM logs +WHERE log_job_id=? +LIMIT 1 +` diff --git a/model/log_test.go b/model/log_test.go new file mode 100644 index 0000000000..af50189e0f --- /dev/null +++ b/model/log_test.go @@ -0,0 +1,59 @@ +package model + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/drone/drone/shared/database" + "github.com/franela/goblin" +) + +func TestLog(t *testing.T) { + db := database.Open("sqlite3", ":memory:") + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Logs", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM logs") + }) + + g.It("Should create a log", func() { + job := Job{ + ID: 1, + } + buf := bytes.NewBufferString("echo hi") + err := SetLog(db, &job, buf) + g.Assert(err == nil).IsTrue() + + rc, err := GetLog(db, &job) + g.Assert(err == nil).IsTrue() + defer rc.Close() + out, _ := ioutil.ReadAll(rc) + g.Assert(string(out)).Equal("echo hi") + }) + + g.It("Should update a log", func() { + job := Job{ + ID: 1, + } + buf1 := bytes.NewBufferString("echo hi") + buf2 := bytes.NewBufferString("echo allo?") + err1 := SetLog(db, &job, buf1) + err2 := SetLog(db, &job, buf2) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + + rc, err := GetLog(db, &job) + g.Assert(err == nil).IsTrue() + defer rc.Close() + out, _ := ioutil.ReadAll(rc) + g.Assert(string(out)).Equal("echo allo?") + }) + + }) +} diff --git a/model/netrc.go b/model/netrc.go new file mode 100644 index 0000000000..f36169a10d --- /dev/null +++ b/model/netrc.go @@ -0,0 +1,7 @@ +package model + +type Netrc struct { + Machine string `json:"machine"` + Login string `json:"login"` + Password string `json:"user"` +} diff --git a/model/node.go b/model/node.go new file mode 100644 index 0000000000..cbad211742 --- /dev/null +++ b/model/node.go @@ -0,0 +1,79 @@ +package model + +import ( + "github.com/drone/drone/shared/database" + "github.com/russross/meddler" +) + +type Node struct { + ID int64 `meddler:"node_id,pk" json:"id"` + Addr string `meddler:"node_addr" json:"address"` + Arch string `meddler:"node_arch" json:"architecture"` + Cert string `meddler:"node_cert" json:"-"` + Key string `meddler:"node_key" json:"-"` + CA string `meddler:"node_ca" json:"-"` +} + +func GetNode(db meddler.DB, id int64) (*Node, error) { + var node = new(Node) + var err = meddler.Load(db, nodeTable, node, id) + return node, err +} + +func GetNodeList(db meddler.DB) ([]*Node, error) { + var nodes = []*Node{} + var err = meddler.QueryAll(db, &nodes, database.Rebind(nodeListQuery)) + return nodes, err +} + +func InsertNode(db meddler.DB, node *Node) error { + return meddler.Insert(db, nodeTable, node) +} + +func UpdateNode(db meddler.DB, node *Node) error { + return meddler.Update(db, nodeTable, node) +} + +func DeleteNode(db meddler.DB, node *Node) error { + var _, err = db.Exec(database.Rebind(nodeDeleteStmt), node.ID) + return err +} + +const nodeTable = "nodes" + +const nodeListQuery = ` +SELECT * +FROM nodes +ORDER BY node_addr +` + +const nodeDeleteStmt = ` +DELETE FROM nodes +WHERE node_id=? +` + +const ( + Freebsd_386 uint = iota + Freebsd_amd64 + Freebsd_arm + Linux_386 + Linux_amd64 + Linux_arm + Linux_arm64 + Solaris_amd64 + Windows_386 + Windows_amd64 +) + +var Archs = map[string]uint{ + "freebsd_386": Freebsd_386, + "freebsd_amd64": Freebsd_amd64, + "freebsd_arm": Freebsd_arm, + "linux_386": Linux_386, + "linux_amd64": Linux_amd64, + "linux_arm": Linux_arm, + "linux_arm64": Linux_arm64, + "solaris_amd64": Solaris_amd64, + "windows_386": Windows_386, + "windows_amd64": Windows_amd64, +} diff --git a/model/node_test.go b/model/node_test.go new file mode 100644 index 0000000000..9de6ec4aa4 --- /dev/null +++ b/model/node_test.go @@ -0,0 +1,100 @@ +package model + +import ( + "testing" + + "github.com/drone/drone/shared/database" + "github.com/franela/goblin" +) + +func TestNode(t *testing.T) { + db := database.Open("sqlite3", ":memory:") + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Nodes", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM nodes") + }) + + g.It("Should create a node", func() { + node := Node{ + Addr: "unix:///var/run/docker/docker.sock", + Arch: "linux_amd64", + } + err := InsertNode(db, &node) + g.Assert(err == nil).IsTrue() + g.Assert(node.ID != 0).IsTrue() + }) + + g.It("Should update a node", func() { + node := Node{ + Addr: "unix:///var/run/docker/docker.sock", + Arch: "linux_amd64", + } + err := InsertNode(db, &node) + g.Assert(err == nil).IsTrue() + g.Assert(node.ID != 0).IsTrue() + + node.Addr = "unix:///var/run/docker.sock" + + err1 := UpdateNode(db, &node) + getnode, err2 := GetNode(db, node.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(node.ID).Equal(getnode.ID) + g.Assert(node.Addr).Equal(getnode.Addr) + g.Assert(node.Arch).Equal(getnode.Arch) + }) + + g.It("Should get a node", func() { + node := Node{ + Addr: "unix:///var/run/docker/docker.sock", + Arch: "linux_amd64", + } + err := InsertNode(db, &node) + g.Assert(err == nil).IsTrue() + g.Assert(node.ID != 0).IsTrue() + + getnode, err := GetNode(db, node.ID) + g.Assert(err == nil).IsTrue() + g.Assert(node.ID).Equal(getnode.ID) + g.Assert(node.Addr).Equal(getnode.Addr) + g.Assert(node.Arch).Equal(getnode.Arch) + }) + + g.It("Should get a node list", func() { + node1 := Node{ + Addr: "unix:///var/run/docker/docker.sock", + Arch: "linux_amd64", + } + node2 := Node{ + Addr: "unix:///var/run/docker.sock", + Arch: "linux_386", + } + InsertNode(db, &node1) + InsertNode(db, &node2) + + nodes, err := GetNodeList(db) + g.Assert(err == nil).IsTrue() + g.Assert(len(nodes)).Equal(2) + }) + + g.It("Should delete a node", func() { + node := Node{ + Addr: "unix:///var/run/docker/docker.sock", + Arch: "linux_amd64", + } + err1 := InsertNode(db, &node) + err2 := DeleteNode(db, &node) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + + _, err := GetNode(db, node.ID) + g.Assert(err == nil).IsFalse() + }) + }) +} diff --git a/model/perm.go b/model/perm.go new file mode 100644 index 0000000000..2e8054658b --- /dev/null +++ b/model/perm.go @@ -0,0 +1,7 @@ +package model + +type Perm struct { + Pull bool `json:"pull"` + Push bool `json:"push"` + Admin bool `json:"admin"` +} diff --git a/model/repo.go b/model/repo.go index dd1e7b8d11..78af7c1d9e 100644 --- a/model/repo.go +++ b/model/repo.go @@ -1,77 +1,89 @@ -package types +package model -type Repo struct { - ID int64 `json:"id"` - UserID int64 `json:"-" sql:"index:ix_repo_user_id"` - Owner string `json:"owner" sql:"unique:ux_repo_owner_name"` - Name string `json:"name" sql:"unique:ux_repo_owner_name"` - FullName string `json:"full_name" sql:"unique:ux_repo_full_name"` - Avatar string `json:"avatar_url"` - Self string `json:"self_url"` - Link string `json:"link_url"` - Clone string `json:"clone_url"` - Branch string `json:"default_branch"` - Private bool `json:"private"` - Trusted bool `json:"trusted"` - Timeout int64 `json:"timeout"` - - Keys *Keypair `json:"-"` - Hooks *Hooks `json:"hooks"` - - // Perms are the current user's permissions to push, - // pull, and administer this repository. The permissions - // are sourced from the version control system (ie GitHub) - Perms *Perm `json:"perms,omitempty" sql:"-"` - - // Params are private environment parameters that are - // considered secret and are therefore stored external - // to the source code repository inside Drone. - Params map[string]string `json:"-"` - - // randomly generated hash used to sign repository - // tokens and encrypt and decrypt private variables. - Hash string `json:"-"` -} +import ( + "github.com/drone/drone/shared/database" + "github.com/russross/meddler" +) type RepoLite struct { - ID int64 `json:"id"` - UserID int64 `json:"-"` Owner string `json:"owner"` Name string `json:"name"` FullName string `json:"full_name"` - Language string `json:"language"` - Private bool `json:"private"` - Created int64 `json:"created_at"` - Updated int64 `json:"updated_at"` + Avatar string `json:"avatar_url"` } -type RepoCommit struct { - ID int64 `json:"id"` - Owner string `json:"owner"` - Name string `json:"name"` - FullName string `json:"full_name"` - Number int `json:"number"` - Status string `json:"status"` - Started int64 `json:"started_at"` - Finished int64 `json:"finished_at"` +type Repo struct { + ID int64 `json:"id" meddler:"repo_id,pk"` + UserID int64 `json:"-" meddler:"repo_user_id"` + Owner string `json:"owner" meddler:"repo_owner"` + Name string `json:"name" meddler:"repo_name"` + FullName string `json:"full_name" meddler:"repo_full_name"` + Avatar string `json:"avatar_url" meddler:"repo_avatar"` + Link string `json:"link_url" meddler:"repo_link"` + Clone string `json:"clone_url" meddler:"repo_clone"` + Branch string `json:"default_branch" meddler:"repo_branch"` + Timeout int64 `json:"timeout" meddler:"repo_timeout"` + IsPrivate bool `json:"private" meddler:"repo_private"` + IsTrusted bool `json:"trusted" meddler:"repo_trusted"` + IsStarred bool `json:"starred,omitempty" meddler:"-"` + AllowPull bool `json:"allow_pr" meddler:"repo_allow_pr"` + AllowPush bool `json:"allow_push" meddler:"repo_allow_push"` + AllowDeploy bool `json:"allow_deploys" meddler:"repo_allow_deploys"` + AllowTag bool `json:"allow_tags" meddler:"repo_allow_tags"` + Hash string `json:"-" meddler:"repo_hash"` +} + +func GetRepo(db meddler.DB, id int64) (*Repo, error) { + var repo = new(Repo) + var err = meddler.Load(db, repoTable, repo, id) + return repo, err } -type Perm struct { - Pull bool `json:"pull" sql:"-"` - Push bool `json:"push" sql:"-"` - Admin bool `json:"admin" sql:"-"` +func GetRepoName(db meddler.DB, owner, name string) (*Repo, error) { + var repo = new(Repo) + var err = meddler.QueryRow(db, repo, database.Rebind(repoNameQuery), owner, name) + return repo, err } -type Hooks struct { - PullRequest bool `json:"pull_request"` - Push bool `json:"push"` - Tags bool `json:"tags"` +func GetRepoList(db meddler.DB, user *User) ([]*Repo, error) { + var repos = []*Repo{} + var err = meddler.QueryAll(db, &repos, database.Rebind(repoListQuery), user.ID) + return repos, err } -// Keypair represents an RSA public and private key -// assigned to a repository. It may be used to clone -// private repositories, or as a deployment key. -type Keypair struct { - Public string `json:"public,omitempty"` - Private string `json:"private,omitempty"` +func CreateRepo(db meddler.DB, repo *Repo) error { + return meddler.Insert(db, repoTable, repo) } + +func UpdateRepo(db meddler.DB, repo *Repo) error { + return meddler.Update(db, repoTable, repo) +} + +func DeleteRepo(db meddler.DB, repo *Repo) error { + var _, err = db.Exec(database.Rebind(repoDeleteStmt), repo.ID) + return err +} + +const repoTable = "repos" + +const repoNameQuery = ` +SELECT * +FROM repos +WHERE repo_owner = ? +AND repo_name = ? +LIMIT 1; +` + +const repoListQuery = ` +SELECT r.* +FROM + repos r +,stars s +WHERE r.repo_id = s.star_repo_id + AND s.star_user_id = ? +` + +const repoDeleteStmt = ` +DELETE FROM repos +WHERE repo_id = ? +` diff --git a/model/repo_test.go b/model/repo_test.go new file mode 100644 index 0000000000..a823728bfe --- /dev/null +++ b/model/repo_test.go @@ -0,0 +1,148 @@ +package model + +import ( + "testing" + + "github.com/drone/drone/shared/database" + "github.com/franela/goblin" +) + +func TestRepostore(t *testing.T) { + db := database.Open("sqlite3", ":memory:") + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Repo", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM stars") + db.Exec("DELETE FROM repos") + db.Exec("DELETE FROM users") + }) + + g.It("Should Set a Repo", func() { + repo := Repo{ + UserID: 1, + FullName: "bradrydzewski/drone", + Owner: "bradrydzewski", + Name: "drone", + } + err1 := CreateRepo(db, &repo) + err2 := UpdateRepo(db, &repo) + getrepo, err3 := GetRepo(db, repo.ID) + if err3 != nil { + println("Get Repo Error") + println(err3.Error()) + } + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(repo.ID).Equal(getrepo.ID) + }) + + g.It("Should Add a Repo", func() { + repo := Repo{ + UserID: 1, + FullName: "bradrydzewski/drone", + Owner: "bradrydzewski", + Name: "drone", + } + err := CreateRepo(db, &repo) + g.Assert(err == nil).IsTrue() + g.Assert(repo.ID != 0).IsTrue() + }) + + g.It("Should Get a Repo by ID", func() { + repo := Repo{ + UserID: 1, + FullName: "bradrydzewski/drone", + Owner: "bradrydzewski", + Name: "drone", + } + CreateRepo(db, &repo) + getrepo, err := GetRepo(db, repo.ID) + g.Assert(err == nil).IsTrue() + g.Assert(repo.ID).Equal(getrepo.ID) + g.Assert(repo.UserID).Equal(getrepo.UserID) + g.Assert(repo.Owner).Equal(getrepo.Owner) + g.Assert(repo.Name).Equal(getrepo.Name) + }) + + g.It("Should Get a Repo by Name", func() { + repo := Repo{ + UserID: 1, + FullName: "bradrydzewski/drone", + Owner: "bradrydzewski", + Name: "drone", + } + CreateRepo(db, &repo) + getrepo, err := GetRepoName(db, repo.Owner, repo.Name) + g.Assert(err == nil).IsTrue() + g.Assert(repo.ID).Equal(getrepo.ID) + g.Assert(repo.UserID).Equal(getrepo.UserID) + g.Assert(repo.Owner).Equal(getrepo.Owner) + g.Assert(repo.Name).Equal(getrepo.Name) + }) + + g.It("Should Get a Repo List by User", func() { + repo1 := Repo{ + UserID: 1, + FullName: "bradrydzewski/drone", + Owner: "bradrydzewski", + Name: "drone", + } + repo2 := Repo{ + UserID: 1, + FullName: "bradrydzewski/drone-dart", + Owner: "bradrydzewski", + Name: "drone-dart", + } + CreateRepo(db, &repo1) + CreateRepo(db, &repo2) + CreateStar(db, &User{ID: 1}, &repo1) + repos, err := GetRepoList(db, &User{ID: 1}) + g.Assert(err == nil).IsTrue() + g.Assert(len(repos)).Equal(1) + g.Assert(repos[0].UserID).Equal(repo1.UserID) + g.Assert(repos[0].Owner).Equal(repo1.Owner) + g.Assert(repos[0].Name).Equal(repo1.Name) + }) + + g.It("Should Delete a Repo", func() { + repo := Repo{ + UserID: 1, + FullName: "bradrydzewski/drone", + Owner: "bradrydzewski", + Name: "drone", + } + CreateRepo(db, &repo) + _, err1 := GetRepo(db, repo.ID) + err2 := DeleteRepo(db, &repo) + _, err3 := GetRepo(db, repo.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsFalse() + }) + + g.It("Should Enforce Unique Repo Name", func() { + repo1 := Repo{ + UserID: 1, + FullName: "bradrydzewski/drone", + Owner: "bradrydzewski", + Name: "drone", + } + repo2 := Repo{ + UserID: 2, + FullName: "bradrydzewski/drone", + Owner: "bradrydzewski", + Name: "drone", + } + err1 := CreateRepo(db, &repo1) + err2 := CreateRepo(db, &repo2) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsFalse() + }) + }) +} diff --git a/model/star.go b/model/star.go new file mode 100644 index 0000000000..3088bc9133 --- /dev/null +++ b/model/star.go @@ -0,0 +1,44 @@ +package model + +import ( + "github.com/drone/drone/shared/database" + "github.com/russross/meddler" +) + +type Star struct { + ID int64 `meddler:"star_id,pk"` + RepoID int64 `meddler:"star_repo_id"` + UserID int64 `meddler:"star_user_id"` +} + +func GetStar(db meddler.DB, user *User, repo *Repo) (bool, error) { + var star = new(Star) + err := meddler.QueryRow(db, star, database.Rebind(starQuery), user.ID, repo.ID) + return (err == nil), err +} + +func CreateStar(db meddler.DB, user *User, repo *Repo) error { + var star = &Star{UserID: user.ID, RepoID: repo.ID} + return meddler.Insert(db, starTable, star) +} + +func DeleteStar(db meddler.DB, user *User, repo *Repo) error { + var _, err = db.Exec(database.Rebind(starDeleteStmt), user.ID, repo.ID) + return err +} + +const starTable = "stars" + +const starQuery = ` +SELECT * +FROM stars +WHERE star_user_id=? +AND star_repo_id=? +LIMIT 1 +` + +const starDeleteStmt = ` +DELETE FROM stars +WHERE star_user_id=? + AND star_repo_id=? +` diff --git a/model/star_test.go b/model/star_test.go new file mode 100644 index 0000000000..2ba98bb963 --- /dev/null +++ b/model/star_test.go @@ -0,0 +1,59 @@ +package model + +import ( + "testing" + + "github.com/drone/drone/shared/database" + "github.com/franela/goblin" +) + +func TestStarstore(t *testing.T) { + db := database.Open("sqlite3", ":memory:") + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Stars", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM stars") + }) + + g.It("Should Add a Star", func() { + user := User{ID: 1} + repo := Repo{ID: 2} + err := CreateStar(db, &user, &repo) + g.Assert(err == nil).IsTrue() + }) + + g.It("Should Get Starred", func() { + user := User{ID: 1} + repo := Repo{ID: 2} + CreateStar(db, &user, &repo) + ok, err := GetStar(db, &user, &repo) + g.Assert(err == nil).IsTrue() + g.Assert(ok).IsTrue() + }) + + g.It("Should Not Get Starred", func() { + user := User{ID: 1} + repo := Repo{ID: 2} + ok, err := GetStar(db, &user, &repo) + g.Assert(err != nil).IsTrue() + g.Assert(ok).IsFalse() + }) + + g.It("Should Del a Star", func() { + user := User{ID: 1} + repo := Repo{ID: 2} + CreateStar(db, &user, &repo) + _, err1 := GetStar(db, &user, &repo) + err2 := DeleteStar(db, &user, &repo) + _, err3 := GetStar(db, &user, &repo) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsFalse() + }) + }) +} diff --git a/model/status.go b/model/status.go deleted file mode 100644 index 780f57a31c..0000000000 --- a/model/status.go +++ /dev/null @@ -1,10 +0,0 @@ -package types - -type Status struct { - ID int64 `json:"-"` - CommitID int64 `json:"-"` - State string `json:"status"` - Link string `json:"target_url"` - Desc string `json:"description"` - Context string `json:"context"` -} diff --git a/model/sys.go b/model/sys.go new file mode 100644 index 0000000000..ce515f1c05 --- /dev/null +++ b/model/sys.go @@ -0,0 +1,8 @@ +package model + +type System struct { + Version string `json:"version"` + Link string `json:"link_url"` + Plugins []string `json:"plugins"` + Globals []string `json:"globals"` +} diff --git a/model/system.go b/model/system.go deleted file mode 100644 index 25a941ae89..0000000000 --- a/model/system.go +++ /dev/null @@ -1,27 +0,0 @@ -package types - -// System provides important information about the Drone -// server to the plugin. -type System struct { - Version string `json:"version"` - Link string `json:"link_url"` - Plugins []string `json:"plugins"` - Globals []string `json:"globals"` -} - -// Workspace defines the build's workspace inside the -// container. This helps the plugin locate the source -// code directory. -type Workspace struct { - Root string `json:"root"` - Path string `json:"path"` - - Netrc *Netrc `json:"netrc"` - Keys *Keypair `json:"keys"` -} - -type Netrc struct { - Machine string `json:"machine"` - Login string `json:"login"` - Password string `json:"user"` -} diff --git a/model/user.go b/model/user.go index 4d0594471a..eb398b1a15 100644 --- a/model/user.go +++ b/model/user.go @@ -1,16 +1,117 @@ -package types +package model + +import ( + "github.com/drone/drone/shared/database" + "github.com/russross/meddler" +) type User struct { - ID int64 `json:"id"` - Login string `json:"login,omitempty" sql:"unique:ux_user_login"` - Token string `json:"-"` - Secret string `json:"-"` - Email string `json:"email,omitempty"` - Avatar string `json:"avatar_url,omitempty"` - Active bool `json:"active,omitempty"` - Admin bool `json:"admin,omitempty"` - - // randomly generated hash used to sign user - // session and application tokens. - Hash string `json:"-"` + ID int64 `json:"id" meddler:"user_id,pk"` + Login string `json:"login" meddler:"user_login"` + Token string `json:"-" meddler:"user_token"` + Secret string `json:"-" meddler:"user_secret"` + Email string `json:"email" meddler:"user_email"` + Avatar string `json:"avatar_url" meddler:"user_avatar"` + Active bool `json:"active," meddler:"user_active"` + Admin bool `json:"admin," meddler:"user_admin"` + Hash string `json:"-" meddler:"user_hash"` +} + +func GetUser(db meddler.DB, id int64) (*User, error) { + var usr = new(User) + var err = meddler.Load(db, userTable, usr, id) + return usr, err +} + +func GetUserLogin(db meddler.DB, login string) (*User, error) { + var usr = new(User) + var err = meddler.QueryRow(db, usr, database.Rebind(userLoginQuery), login) + return usr, err +} + +func GetUserList(db meddler.DB) ([]*User, error) { + var users = []*User{} + var err = meddler.QueryAll(db, &users, database.Rebind(userListQuery)) + return users, err +} + +func GetUserFeed(db meddler.DB, user *User, limit, offset int) ([]*Feed, error) { + var feed = []*Feed{} + var err = meddler.QueryAll(db, &feed, database.Rebind(userFeedQuery), user.ID, limit, offset) + return feed, err } + +func GetUserCount(db meddler.DB) (int, error) { + var count int + var err = db.QueryRow(database.Rebind(userCountQuery)).Scan(&count) + return count, err +} + +func CreateUser(db meddler.DB, user *User) error { + return meddler.Insert(db, userTable, user) +} + +func UpdateUser(db meddler.DB, user *User) error { + return meddler.Update(db, userTable, user) +} + +func DeleteUser(db meddler.DB, user *User) error { + var _, err = db.Exec(database.Rebind(userDeleteStmt), user.ID) + return err +} + +const userTable = "users" + +const userLoginQuery = ` +SELECT * +FROM users +WHERE user_login=? +LIMIT 1 +` + +const userListQuery = ` +SELECT * +FROM users +ORDER BY user_login ASC +` + +const userCountQuery = ` +SELECT count(1) +FROM users +` + +const userDeleteStmt = ` +DELETE FROM users +WHERE user_id=? +` + +const userFeedQuery = ` +SELECT + repo_owner +,repo_name +,repo_full_name +,repo_avatar +,build_number +,build_event +,build_status +,build_started +,build_finished +,build_commit +,build_branch +,build_ref +,build_refspec +,build_remote +,build_title +,build_message +,build_author +,build_email +FROM + builds b +,repos r +,stars s +WHERE b.build_repo_id = r.repo_id + AND r.repo_id = s.star_repo_id + AND s.star_user_id = ? +ORDER BY b.build_number DESC +LIMIT ? OFFSET ? +` diff --git a/model/user_test.go b/model/user_test.go new file mode 100644 index 0000000000..0fa1a91e0c --- /dev/null +++ b/model/user_test.go @@ -0,0 +1,207 @@ +package model + +import ( + "testing" + + "github.com/drone/drone/shared/database" + "github.com/franela/goblin" +) + +func TestUserstore(t *testing.T) { + db := database.Open("sqlite3", ":memory:") + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("User", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM users") + db.Exec("DELETE FROM stars") + db.Exec("DELETE FROM repos") + db.Exec("DELETE FROM builds") + db.Exec("DELETE FROM jobs") + }) + + g.It("Should Update a User", func() { + user := User{ + Login: "joe", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + err1 := CreateUser(db, &user) + err2 := UpdateUser(db, &user) + getuser, err3 := GetUser(db, user.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(user.ID).Equal(getuser.ID) + }) + + g.It("Should Add a new User", func() { + user := User{ + Login: "joe", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + err := CreateUser(db, &user) + g.Assert(err == nil).IsTrue() + g.Assert(user.ID != 0).IsTrue() + }) + + g.It("Should Get a User", func() { + user := User{ + Login: "joe", + Token: "f0b461ca586c27872b43a0685cbc2847", + Secret: "976f22a5eef7caacb7e678d6c52f49b1", + Email: "foo@bar.com", + Avatar: "b9015b0857e16ac4d94a0ffd9a0b79c8", + Active: true, + Admin: true, + } + + CreateUser(db, &user) + getuser, err := GetUser(db, user.ID) + g.Assert(err == nil).IsTrue() + g.Assert(user.ID).Equal(getuser.ID) + g.Assert(user.Login).Equal(getuser.Login) + g.Assert(user.Token).Equal(getuser.Token) + g.Assert(user.Secret).Equal(getuser.Secret) + g.Assert(user.Email).Equal(getuser.Email) + g.Assert(user.Avatar).Equal(getuser.Avatar) + g.Assert(user.Active).Equal(getuser.Active) + g.Assert(user.Admin).Equal(getuser.Admin) + }) + + g.It("Should Get a User By Login", func() { + user := User{ + Login: "joe", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + CreateUser(db, &user) + getuser, err := GetUserLogin(db, user.Login) + g.Assert(err == nil).IsTrue() + g.Assert(user.ID).Equal(getuser.ID) + g.Assert(user.Login).Equal(getuser.Login) + }) + + g.It("Should Enforce Unique User Login", func() { + user1 := User{ + Login: "joe", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + user2 := User{ + Login: "joe", + Email: "foo@bar.com", + Token: "ab20g0ddaf012c744e136da16aa21ad9", + } + err1 := CreateUser(db, &user1) + err2 := CreateUser(db, &user2) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsFalse() + }) + + g.It("Should Get a User List", func() { + user1 := User{ + Login: "jane", + Email: "foo@bar.com", + Token: "ab20g0ddaf012c744e136da16aa21ad9", + } + user2 := User{ + Login: "joe", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + CreateUser(db, &user1) + CreateUser(db, &user2) + users, err := GetUserList(db) + g.Assert(err == nil).IsTrue() + g.Assert(len(users)).Equal(2) + g.Assert(users[0].Login).Equal(user1.Login) + g.Assert(users[0].Email).Equal(user1.Email) + g.Assert(users[0].Token).Equal(user1.Token) + }) + + g.It("Should Get a User Count", func() { + user1 := User{ + Login: "jane", + Email: "foo@bar.com", + Token: "ab20g0ddaf012c744e136da16aa21ad9", + } + user2 := User{ + Login: "joe", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + CreateUser(db, &user1) + CreateUser(db, &user2) + count, err := GetUserCount(db) + g.Assert(err == nil).IsTrue() + g.Assert(count).Equal(2) + }) + + g.It("Should Get a User Count Zero", func() { + count, err := GetUserCount(db) + g.Assert(err == nil).IsTrue() + g.Assert(count).Equal(0) + }) + + g.It("Should Del a User", func() { + user := User{ + Login: "joe", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + CreateUser(db, &user) + _, err1 := GetUser(db, user.ID) + err2 := DeleteUser(db, &user) + _, err3 := GetUser(db, user.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsFalse() + }) + + g.It("Should get the Build feed for a User", func() { + repo1 := &Repo{ + UserID: 1, + Owner: "bradrydzewski", + Name: "drone", + FullName: "bradrydzewski/drone", + } + repo2 := &Repo{ + UserID: 2, + Owner: "drone", + Name: "drone", + FullName: "drone/drone", + } + CreateRepo(db, repo1) + CreateRepo(db, repo2) + CreateStar(db, &User{ID: 1}, repo1) + + build1 := &Build{ + RepoID: repo1.ID, + Status: StatusFailure, + } + build2 := &Build{ + RepoID: repo1.ID, + Status: StatusSuccess, + } + build3 := &Build{ + RepoID: repo2.ID, + Status: StatusSuccess, + } + CreateBuild(db, build1) + CreateBuild(db, build2) + CreateBuild(db, build3) + + builds, err := GetUserFeed(db, &User{ID: 1}, 20, 0) + g.Assert(err == nil).IsTrue() + g.Assert(len(builds)).Equal(2) + g.Assert(builds[0].Owner).Equal("bradrydzewski") + g.Assert(builds[0].Name).Equal("drone") + }) + }) +} diff --git a/model/util.go b/model/util.go deleted file mode 100644 index 951e8b4079..0000000000 --- a/model/util.go +++ /dev/null @@ -1,36 +0,0 @@ -package types - -import ( - "crypto/rand" - "io" -) - -// standard characters allowed in token string. -var chars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") - -// default token length -var length = 32 - -// GenerateToken generates random strings good for use in URIs to -// identify unique objects. -func GenerateToken() string { - b := make([]byte, length) - r := make([]byte, length+(length/4)) // storage for random bytes. - clen := byte(len(chars)) - maxrb := byte(256 - (256 % len(chars))) - i := 0 - for { - io.ReadFull(rand.Reader, r) - for _, c := range r { - if c >= maxrb { - // Skip this number to avoid modulo bias. - continue - } - b[i] = chars[c%clen] - i++ - if i == length { - return string(b) - } - } - } -} diff --git a/model/util_test.go b/model/util_test.go deleted file mode 100644 index 36f849b05d..0000000000 --- a/model/util_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package types - -import ( - "testing" -) - -func Test_GenerateToken(t *testing.T) { - token := GenerateToken() - if len(token) != length { - t.Errorf("Want token length %d, got %d", length, len(token)) - } -} diff --git a/remote/github/github.go b/remote/github/github.go new file mode 100644 index 0000000000..763000ca7c --- /dev/null +++ b/remote/github/github.go @@ -0,0 +1,436 @@ +package github + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/drone/drone/model" + "github.com/drone/drone/shared/envconfig" + "github.com/drone/drone/shared/httputil" + "github.com/drone/drone/shared/oauth2" + + log "github.com/Sirupsen/logrus" + "github.com/google/go-github/github" +) + +const ( + DefaultURL = "https://github.com" + DefaultAPI = "https://api.github.com" + DefaultScope = "repo,repo:status,user:email" +) + +type Github struct { + URL string + API string + Client string + Secret string + Orgs []string + Open bool + PrivateMode bool + SkipVerify bool +} + +func Load(env envconfig.Env) *Github { + config := env.String("REMOTE_CONFIG", "") + + // parse the remote DSN configuration string + url_, err := url.Parse(config) + if err != nil { + log.Fatalln("unable to parse remote dsn. %s", err) + } + params := url_.Query() + url_.Path = "" + url_.RawQuery = "" + + // create the Githbub remote using parameters from + // the parsed DSN configuration string. + github := Github{} + github.URL = url_.String() + github.Client = params.Get("client_id") + github.Secret = params.Get("client_secret") + github.Orgs = params["orgs"] + github.PrivateMode, _ = strconv.ParseBool(params.Get("private_mode")) + github.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify")) + github.Open, _ = strconv.ParseBool(params.Get("open")) + + if github.URL == DefaultURL { + github.API = DefaultAPI + } else { + github.API = github.URL + "/api/v3/" + } + + return &github +} + +// Login authenticates the session and returns the +// remote user details. +func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { + + var config = &oauth2.Config{ + ClientId: g.Client, + ClientSecret: g.Secret, + Scope: DefaultScope, + AuthURL: fmt.Sprintf("%s/login/oauth/authorize", g.URL), + TokenURL: fmt.Sprintf("%s/login/oauth/access_token", g.URL), + RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(req)), + } + + // get the OAuth code + var code = req.FormValue("code") + if len(code) == 0 { + var random = GetRandom() + http.Redirect(res, req, config.AuthCodeURL(random), http.StatusSeeOther) + return nil, false, nil + } + + var trans = &oauth2.Transport{Config: config} + var token, err = trans.Exchange(code) + if err != nil { + return nil, false, fmt.Errorf("Error exchanging token. %s", err) + } + + var client = NewClient(g.API, token.AccessToken, g.SkipVerify) + var useremail, errr = GetUserEmail(client) + if errr != nil { + return nil, false, fmt.Errorf("Error retrieving user or verified email. %s", errr) + } + + if len(g.Orgs) > 0 { + allowedOrg, err := UserBelongsToOrg(client, g.Orgs) + if err != nil { + return nil, false, fmt.Errorf("Could not check org membership. %s", err) + } + if !allowedOrg { + return nil, false, fmt.Errorf("User does not belong to correct org. Must belong to %v", g.Orgs) + } + } + + user := model.User{} + user.Login = *useremail.Login + user.Email = *useremail.Email + user.Token = token.AccessToken + user.Avatar = *useremail.AvatarURL + return &user, g.Open, nil +} + +// Auth authenticates the session and returns the remote user +// login for the given token and secret +func (g *Github) Auth(token, secret string) (string, error) { + client := NewClient(g.API, token, g.SkipVerify) + user, _, err := client.Users.Get("") + if err != nil { + return "", err + } + return *user.Login, nil +} + +// Repo fetches the named repository from the remote system. +func (g *Github) Repo(u *model.User, owner, name string) (*model.Repo, error) { + client := NewClient(g.API, u.Token, g.SkipVerify) + repo_, err := GetRepo(client, owner, name) + if err != nil { + return nil, err + } + + repo := &model.Repo{} + repo.Owner = owner + repo.Name = name + repo.FullName = *repo_.FullName + repo.Link = *repo_.HTMLURL + repo.IsPrivate = *repo_.Private + repo.Clone = *repo_.CloneURL + repo.Branch = "master" + repo.Avatar = *repo_.Owner.AvatarURL + + if repo_.DefaultBranch != nil { + repo.Branch = *repo_.DefaultBranch + } + + if g.PrivateMode { + repo.IsPrivate = true + } + return repo, err +} + +// Repos fetches a list of repos from the remote system. +func (g *Github) Repos(u *model.User) ([]*model.RepoLite, error) { + client := NewClient(g.API, u.Token, g.SkipVerify) + + all, err := GetAllRepos(client) + if err != nil { + return nil, err + } + + var repos = []*model.RepoLite{} + for _, repo := range all { + repos = append(repos, &model.RepoLite{ + Owner: *repo.Owner.Login, + Name: *repo.Name, + FullName: *repo.FullName, + Avatar: *repo.Owner.AvatarURL, + }) + } + return repos, err +} + +// Perm fetches the named repository permissions from +// the remote system for the specified user. +func (g *Github) Perm(u *model.User, owner, name string) (*model.Perm, error) { + + client := NewClient(g.API, u.Token, g.SkipVerify) + repo, err := GetRepo(client, owner, name) + if err != nil { + return nil, err + } + m := &model.Perm{} + m.Admin = (*repo.Permissions)["admin"] + m.Push = (*repo.Permissions)["push"] + m.Pull = (*repo.Permissions)["pull"] + return m, nil +} + +// Script fetches the build script (.drone.yml) from the remote +// repository and returns in string format. +func (g *Github) Script(u *model.User, r *model.Repo, b *model.Build) ([]byte, []byte, error) { + client := NewClient(g.API, u.Token, g.SkipVerify) + + cfg, err := GetFile(client, r.Owner, r.Name, ".drone.yml", b.Commit) + sec, _ := GetFile(client, r.Owner, r.Name, ".drone.sec", b.Commit) + return cfg, sec, err +} + +// Status sends the commit status to the remote system. +// An example would be the GitHub pull request status. +func (g *Github) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { + client := NewClient(g.API, u.Token, g.SkipVerify) + + status := getStatus(b.Status) + desc := getDesc(b.Status) + data := github.RepoStatus{ + Context: github.String("Drone"), + State: github.String(status), + Description: github.String(desc), + TargetURL: github.String(link), + } + _, _, err := client.Repositories.CreateStatus(r.Owner, r.Name, b.Commit, &data) + return err +} + +// Netrc returns a .netrc file that can be used to clone +// private repositories from a remote system. +func (g *Github) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { + url_, err := url.Parse(g.URL) + if err != nil { + return nil, err + } + netrc := &model.Netrc{} + netrc.Login = u.Token + netrc.Password = "x-oauth-basic" + netrc.Machine = url_.Host + return netrc, nil +} + +// Activate activates a repository by creating the post-commit hook and +// adding the SSH deploy key, if applicable. +func (g *Github) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { + client := NewClient(g.API, u.Token, g.SkipVerify) + title, err := GetKeyTitle(link) + if err != nil { + return err + } + + // if the CloneURL is using the SSHURL then we know that + // we need to add an SSH key to GitHub. + if r.IsPrivate || g.PrivateMode { + _, err = CreateUpdateKey(client, r.Owner, r.Name, title, k.Public) + if err != nil { + return err + } + } + + _, err = CreateUpdateHook(client, r.Owner, r.Name, link) + return err +} + +// Deactivate removes a repository by removing all the post-commit hooks +// which are equal to link and removing the SSH deploy key. +func (g *Github) Deactivate(u *model.User, r *model.Repo, link string) error { + client := NewClient(g.API, u.Token, g.SkipVerify) + title, err := GetKeyTitle(link) + if err != nil { + return err + } + + // remove the deploy-key if it is installed remote. + if r.IsPrivate || g.PrivateMode { + if err := DeleteKey(client, r.Owner, r.Name, title); err != nil { + return err + } + } + + return DeleteHook(client, r.Owner, r.Name, link) +} + +// Hook parses the post-commit hook from the Request body +// and returns the required data in a standard format. +func (g *Github) Hook(r *http.Request) (*model.Repo, *model.Build, error) { + + switch r.Header.Get("X-Github-Event") { + case "pull_request": + return g.pullRequest(r) + case "push": + return g.push(r) + default: + return nil, nil, nil + } +} + +// push parses a hook with event type `push` and returns +// the commit data. +func (g *Github) push(r *http.Request) (*model.Repo, *model.Build, error) { + payload := GetPayload(r) + hook := &pushHook{} + err := json.Unmarshal(payload, hook) + if err != nil { + return nil, nil, err + } + if hook.Deleted { + return nil, nil, err + } + + repo := &model.Repo{} + repo.Owner = hook.Repo.Owner.Login + if len(repo.Owner) == 0 { + repo.Owner = hook.Repo.Owner.Name + } + repo.Name = hook.Repo.Name + // Generating rather than using hook.Repo.FullName as it's + // not always present + repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name) + repo.Link = hook.Repo.HTMLURL + repo.IsPrivate = hook.Repo.Private + repo.Clone = hook.Repo.CloneURL + repo.Branch = hook.Repo.DefaultBranch + + build := &model.Build{} + build.Event = model.EventPush + build.Commit = hook.Head.ID + build.Ref = hook.Ref + build.Link = hook.Head.URL + build.Branch = strings.Replace(build.Ref, "refs/heads/", "", -1) + build.Message = hook.Head.Message + // build.Timestamp = hook.Head.Timestamp + // build.Email = hook.Head.Author.Email + build.Avatar = hook.Sender.Avatar + build.Author = hook.Sender.Login + build.Remote = hook.Repo.CloneURL + + // we should ignore github pages + if build.Ref == "refs/heads/gh-pages" { + return nil, nil, nil + } + + return repo, build, nil +} + +// pullRequest parses a hook with event type `pullRequest` +// and returns the commit data. +func (g *Github) pullRequest(r *http.Request) (*model.Repo, *model.Build, error) { + payload := GetPayload(r) + hook := &struct { + Action string `json:"action"` + PullRequest *github.PullRequest `json:"pull_request"` + Repo *github.Repository `json:"repository"` + }{} + err := json.Unmarshal(payload, hook) + if err != nil { + return nil, nil, err + } + + // ignore these + if hook.Action != "opened" && hook.Action != "synchronize" { + return nil, nil, nil + } + if *hook.PullRequest.State != "open" { + return nil, nil, nil + } + + repo := &model.Repo{} + repo.Owner = *hook.Repo.Owner.Login + repo.Name = *hook.Repo.Name + repo.FullName = *hook.Repo.FullName + repo.Link = *hook.Repo.HTMLURL + repo.IsPrivate = *hook.Repo.Private + repo.Clone = *hook.Repo.CloneURL + repo.Branch = "master" + if hook.Repo.DefaultBranch != nil { + repo.Branch = *hook.Repo.DefaultBranch + } + + build := &model.Build{} + build.Event = model.EventPull + build.Commit = *hook.PullRequest.Head.SHA + build.Ref = fmt.Sprintf("refs/pull/%d/merge", *hook.PullRequest.Number) + build.Link = *hook.PullRequest.HTMLURL + build.Branch = *hook.PullRequest.Head.Ref + build.Message = *hook.PullRequest.Title + build.Author = *hook.PullRequest.Head.User.Login + build.Avatar = *hook.PullRequest.Head.User.AvatarURL + build.Remote = *hook.PullRequest.Base.Repo.CloneURL + build.Title = *hook.PullRequest.Title + // build.Timestamp = time.Now().UTC().Format("2006-01-02 15:04:05.000000000 +0000 MST") + + return repo, build, nil +} + +const ( + StatusPending = "pending" + StatusSuccess = "success" + StatusFailure = "failure" + StatusError = "error" +) + +const ( + DescPending = "this build is pending" + DescSuccess = "the build was successful" + DescFailure = "the build failed" + DescError = "oops, something went wrong" +) + +// getStatus is a helper functin that converts a Drone +// status to a GitHub status. +func getStatus(status string) string { + switch status { + case model.StatusPending, model.StatusRunning: + return StatusPending + case model.StatusSuccess: + return StatusSuccess + case model.StatusFailure: + return StatusFailure + case model.StatusError, model.StatusKilled: + return StatusError + default: + return StatusError + } +} + +// getDesc is a helper function that generates a description +// message for the build based on the status. +func getDesc(status string) string { + switch status { + case model.StatusPending, model.StatusRunning: + return DescPending + case model.StatusSuccess: + return DescSuccess + case model.StatusFailure: + return DescFailure + case model.StatusError, model.StatusKilled: + return DescError + default: + return DescError + } +} diff --git a/remote/github/github/github.go b/remote/github/github/github.go deleted file mode 100644 index 54b261b6aa..0000000000 --- a/remote/github/github/github.go +++ /dev/null @@ -1,478 +0,0 @@ -package github - -import ( - "crypto/tls" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/hashicorp/golang-lru" - "github.com/drone/drone/pkg/oauth2" - "github.com/drone/drone/pkg/remote" - common "github.com/drone/drone/pkg/types" - "github.com/drone/drone/pkg/utils/httputil" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/google/go-github/github" -) - -const ( - DefaultURL = "https://github.com" - DefaultAPI = "https://api.github.com" - DefaultScope = "repo,repo:status,user:email" -) - -type GitHub struct { - URL string - API string - Client string - Secret string - AllowedOrgs []string - Open bool - PrivateMode bool - SkipVerify bool - - cache *lru.Cache -} - -func init() { - remote.Register("github", NewDriver) -} - -func NewDriver(config string) (remote.Remote, error) { - url_, err := url.Parse(config) - if err != nil { - return nil, err - } - params := url_.Query() - url_.Path = "" - url_.RawQuery = "" - - github := GitHub{} - github.URL = url_.String() - github.Client = params.Get("client_id") - github.Secret = params.Get("client_secret") - github.AllowedOrgs = params["orgs"] - github.PrivateMode, _ = strconv.ParseBool(params.Get("private_mode")) - github.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify")) - github.Open, _ = strconv.ParseBool(params.Get("open")) - - if github.URL == DefaultURL { - github.API = DefaultAPI - } else { - github.API = github.URL + "/api/v3/" - } - - // here we cache permissions to avoid too many api - // calls. this should really be moved outise the - // remote plugin into the app - github.cache, err = lru.New(1028) - return &github, err -} - -func (g *GitHub) Login(token, secret string) (*common.User, error) { - client := NewClient(g.API, token, g.SkipVerify) - login, err := GetUserEmail(client) - if err != nil { - return nil, err - } - user := common.User{} - user.Login = *login.Login - user.Email = *login.Email - user.Token = token - user.Secret = secret - user.Avatar = *login.AvatarURL - return &user, nil -} - -// Orgs fetches the organizations for the given user. -func (g *GitHub) Orgs(u *common.User) ([]string, error) { - client := NewClient(g.API, u.Token, g.SkipVerify) - orgs_ := []string{} - orgs, err := GetOrgs(client) - if err != nil { - return orgs_, err - } - for _, org := range orgs { - orgs_ = append(orgs_, *org.Login) - } - return orgs_, nil -} - -// Accessor method, to allowed remote organizations field. -func (g *GitHub) GetOrgs() []string { - return g.AllowedOrgs -} - -// Accessor method, to open field. -func (g *GitHub) GetOpen() bool { - return g.Open -} - -// Repo fetches the named repository from the remote system. -func (g *GitHub) Repo(u *common.User, owner, name string) (*common.Repo, error) { - client := NewClient(g.API, u.Token, g.SkipVerify) - repo_, err := GetRepo(client, owner, name) - if err != nil { - return nil, err - } - - repo := &common.Repo{} - repo.Owner = owner - repo.Name = name - repo.FullName = *repo_.FullName - repo.Link = *repo_.HTMLURL - repo.Private = *repo_.Private - repo.Clone = *repo_.CloneURL - repo.Branch = "master" - repo.Avatar = *repo_.Owner.AvatarURL - - if repo_.DefaultBranch != nil { - repo.Branch = *repo_.DefaultBranch - } - - if g.PrivateMode { - repo.Private = true - } - return repo, err -} - -// Perm fetches the named repository from the remote system. -func (g *GitHub) Perm(u *common.User, owner, name string) (*common.Perm, error) { - key := fmt.Sprintf("%s/%s/%s", u.Login, owner, name) - val, ok := g.cache.Get(key) - if ok { - return val.(*common.Perm), nil - } - - client := NewClient(g.API, u.Token, g.SkipVerify) - repo, err := GetRepo(client, owner, name) - if err != nil { - return nil, err - } - m := &common.Perm{} - m.Admin = (*repo.Permissions)["admin"] - m.Push = (*repo.Permissions)["push"] - m.Pull = (*repo.Permissions)["pull"] - g.cache.Add(key, m) - return m, nil -} - -// Script fetches the build script (.drone.yml) from the remote -// repository and returns in string format. -func (g *GitHub) Script(u *common.User, r *common.Repo, b *common.Build) ([]byte, []byte, error) { - client := NewClient(g.API, u.Token, g.SkipVerify) - - cfg, err := GetFile(client, r.Owner, r.Name, ".drone.yml", b.Commit.Sha) - sec, _ := GetFile(client, r.Owner, r.Name, ".drone.sec", b.Commit.Sha) - return cfg, sec, err -} - -// Netrc returns a .netrc file that can be used to clone -// private repositories from a remote system. -func (g *GitHub) Netrc(u *common.User, r *common.Repo) (*common.Netrc, error) { - url_, err := url.Parse(g.URL) - if err != nil { - return nil, err - } - netrc := &common.Netrc{} - netrc.Login = u.Token - netrc.Password = "x-oauth-basic" - netrc.Machine = url_.Host - return netrc, nil -} - -// Activate activates a repository by creating the post-commit hook and -// adding the SSH deploy key, if applicable. -func (g *GitHub) Activate(u *common.User, r *common.Repo, k *common.Keypair, link string) error { - client := NewClient(g.API, u.Token, g.SkipVerify) - title, err := GetKeyTitle(link) - if err != nil { - return err - } - - // if the CloneURL is using the SSHURL then we know that - // we need to add an SSH key to GitHub. - if r.Private || g.PrivateMode { - _, err = CreateUpdateKey(client, r.Owner, r.Name, title, k.Public) - if err != nil { - return err - } - } - - _, err = CreateUpdateHook(client, r.Owner, r.Name, link) - return err -} - -// Deactivate removes a repository by removing all the post-commit hooks -// which are equal to link and removing the SSH deploy key. -func (g *GitHub) Deactivate(u *common.User, r *common.Repo, link string) error { - client := NewClient(g.API, u.Token, g.SkipVerify) - title, err := GetKeyTitle(link) - if err != nil { - return err - } - - // remove the deploy-key if it is installed remote. - if r.Private || g.PrivateMode { - if err := DeleteKey(client, r.Owner, r.Name, title); err != nil { - return err - } - } - - return DeleteHook(client, r.Owner, r.Name, link) -} - -func (g *GitHub) Status(u *common.User, r *common.Repo, b *common.Build) error { - client := NewClient(g.API, u.Token, g.SkipVerify) - - link := fmt.Sprintf("%s/%v", r.Self, b.Number) - status := getStatus(b.Status) - desc := getDesc(b.Status) - data := github.RepoStatus{ - Context: github.String("Drone"), - State: github.String(status), - Description: github.String(desc), - TargetURL: github.String(link), - } - _, _, err := client.Repositories.CreateStatus(r.Owner, r.Name, b.Commit.Sha, &data) - return err -} - -// Hook parses the post-commit hook from the Request body -// and returns the required data in a standard format. -func (g *GitHub) Hook(r *http.Request) (*common.Hook, error) { - switch r.Header.Get("X-Github-Event") { - case "pull_request": - return g.pullRequest(r) - case "push": - return g.push(r) - default: - return nil, nil - } -} - -// return default scope for GitHub -func (g *GitHub) Scope() string { - return DefaultScope -} - -// push parses a hook with event type `push` and returns -// the commit data. -func (g *GitHub) push(r *http.Request) (*common.Hook, error) { - payload := GetPayload(r) - hook := &pushHook{} - err := json.Unmarshal(payload, hook) - if err != nil { - return nil, err - } - - if hook.Deleted { - return nil, nil - } - - repo := &common.Repo{} - repo.Owner = hook.Repo.Owner.Login - if len(repo.Owner) == 0 { - repo.Owner = hook.Repo.Owner.Name - } - repo.Name = hook.Repo.Name - // Generating rather than using hook.Repo.FullName as it's - // not always present - repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name) - repo.Link = hook.Repo.HTMLURL - repo.Private = hook.Repo.Private - repo.Clone = hook.Repo.CloneURL - repo.Branch = hook.Repo.DefaultBranch - - commit := &common.Commit{} - commit.Sha = hook.Head.ID - commit.Ref = hook.Ref - commit.Link = hook.Head.URL - commit.Branch = strings.Replace(commit.Ref, "refs/heads/", "", -1) - commit.Message = hook.Head.Message - commit.Timestamp = hook.Head.Timestamp - commit.Author = &common.Author{} - commit.Author.Email = hook.Head.Author.Email - commit.Author.Login = hook.Head.Author.Username - commit.Remote = hook.Repo.CloneURL - - // we should ignore github pages - if commit.Ref == "refs/heads/gh-pages" { - return nil, nil - } - - return &common.Hook{Event: "push", Repo: repo, Commit: commit}, nil -} - -// ¯\_(ツ)_/¯ -func (g *GitHub) Oauth2Transport(r *http.Request) *oauth2.Transport { - return &oauth2.Transport{ - Config: &oauth2.Config{ - ClientId: g.Client, - ClientSecret: g.Secret, - Scope: DefaultScope, - AuthURL: fmt.Sprintf("%s/login/oauth/authorize", g.URL), - TokenURL: fmt.Sprintf("%s/login/oauth/access_token", g.URL), - RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(r)), - //settings.Server.Scheme, settings.Server.Hostname), - }, - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{InsecureSkipVerify: g.SkipVerify}, - }, - } -} - -// pullRequest parses a hook with event type `pullRequest` -// and returns the commit data. -func (g *GitHub) pullRequest(r *http.Request) (*common.Hook, error) { - payload := GetPayload(r) - hook := &struct { - Action string `json:"action"` - PullRequest *github.PullRequest `json:"pull_request"` - Repo *github.Repository `json:"repository"` - }{} - err := json.Unmarshal(payload, hook) - if err != nil { - return nil, err - } - - // ignore these - if hook.Action != "opened" && hook.Action != "synchronize" { - return nil, nil - } - if *hook.PullRequest.State != "open" { - return nil, nil - } - - repo := &common.Repo{} - repo.Owner = *hook.Repo.Owner.Login - repo.Name = *hook.Repo.Name - repo.FullName = *hook.Repo.FullName - repo.Link = *hook.Repo.HTMLURL - repo.Private = *hook.Repo.Private - repo.Clone = *hook.Repo.CloneURL - repo.Branch = "master" - if hook.Repo.DefaultBranch != nil { - repo.Branch = *hook.Repo.DefaultBranch - } - - c := &common.Commit{} - c.Sha = *hook.PullRequest.Head.SHA - c.Ref = *hook.PullRequest.Head.Ref - c.Ref = fmt.Sprintf("refs/pull/%d/merge", *hook.PullRequest.Number) - c.Branch = *hook.PullRequest.Head.Ref - c.Timestamp = time.Now().UTC().Format("2006-01-02 15:04:05.000000000 +0000 MST") - c.Remote = *hook.PullRequest.Head.Repo.CloneURL - c.Author = &common.Author{} - c.Author.Login = *hook.PullRequest.Head.User.Login - - // Author.Email - // Message - - pr := &common.PullRequest{} - pr.Number = *hook.PullRequest.Number - pr.Title = *hook.PullRequest.Title - pr.Base = &common.Commit{} - pr.Base.Sha = *hook.PullRequest.Base.SHA - pr.Base.Ref = *hook.PullRequest.Base.Ref - pr.Base.Remote = *hook.PullRequest.Base.Repo.CloneURL - pr.Link = *hook.PullRequest.HTMLURL - // Branch - // Message - // Timestamp - // Author.Login - // Author.Email - - return &common.Hook{Event: "pull_request", Repo: repo, Commit: c, PullRequest: pr}, nil -} - -type pushHook struct { - Ref string `json:"ref"` - Deleted bool `json:"deleted"` - - Head struct { - ID string `json:"id"` - URL string `json:"url"` - Message string `json:"message"` - Timestamp string `json:"timestamp"` - - Author struct { - Name string `json:"name"` - Email string `json:"email"` - Username string `json:"username"` - } `json:"author"` - - Committer struct { - Name string `json:"name"` - Email string `json:"email"` - Username string `json:"username"` - } `json:"committer"` - } `json:"head_commit"` - - Repo struct { - Owner struct { - Login string `json:"login"` - Name string `json:"name"` - } `json:"owner"` - Name string `json:"name"` - FullName string `json:"full_name"` - Language string `json:"language"` - Private bool `json:"private"` - HTMLURL string `json:"html_url"` - CloneURL string `json:"clone_url"` - DefaultBranch string `json:"default_branch"` - } `json:"repository"` -} - -const ( - StatusPending = "pending" - StatusSuccess = "success" - StatusFailure = "failure" - StatusError = "error" -) - -const ( - DescPending = "this build is pending" - DescSuccess = "the build was successful" - DescFailure = "the build failed" - DescError = "oops, something went wrong" -) - -// getStatus is a helper functin that converts a Drone -// status to a GitHub status. -func getStatus(status string) string { - switch status { - case common.StatePending, common.StateRunning: - return StatusPending - case common.StateSuccess: - return StatusSuccess - case common.StateFailure: - return StatusFailure - case common.StateError, common.StateKilled: - return StatusError - default: - return StatusError - } -} - -// getDesc is a helper function that generates a description -// message for the build based on the status. -func getDesc(status string) string { - switch status { - case common.StatePending, common.StateRunning: - return DescPending - case common.StateSuccess: - return DescSuccess - case common.StateFailure: - return DescFailure - case common.StateError, common.StateKilled: - return DescError - default: - return DescError - } -} diff --git a/remote/github/github/helper.go b/remote/github/helper.go similarity index 97% rename from remote/github/github/helper.go rename to remote/github/helper.go index bedd39ceb3..42e84f9e46 100644 --- a/remote/github/github/helper.go +++ b/remote/github/helper.go @@ -9,9 +9,9 @@ import ( "net/url" "strings" - "github.com/drone/drone/Godeps/_workspace/src/github.com/google/go-github/github" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gorilla/securecookie" - "github.com/drone/drone/pkg/oauth2" + "github.com/drone/drone/shared/oauth2" + "github.com/google/go-github/github" + "github.com/gorilla/securecookie" ) // NewClient is a helper function that returns a new GitHub diff --git a/remote/github/types.go b/remote/github/types.go new file mode 100644 index 0000000000..f298f5852b --- /dev/null +++ b/remote/github/types.go @@ -0,0 +1,48 @@ +package github + +type postHook struct { +} + +type pushHook struct { + Ref string `json:"ref"` + Deleted bool `json:"deleted"` + + Head struct { + ID string `json:"id"` + URL string `json:"url"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + + Author struct { + Name string `json:"name"` + Email string `json:"name"` + Username string `json:"username"` + } `json:"author"` + + Committer struct { + Name string `json:"name"` + Email string `json:"name"` + Username string `json:"username"` + } `json:"committer"` + } `json:"head_commit"` + + Sender struct { + Login string `json:"login"` + Avatar string `json:"avatar_url"` + } + + Repo struct { + Owner struct { + Login string `json:"login"` + Name string `json:"name"` + } `json:"owner"` + + Name string `json:"name"` + FullName string `json:"full_name"` + Language string `json:"language"` + Private bool `json:"private"` + HTMLURL string `json:"html_url"` + CloneURL string `json:"clone_url"` + DefaultBranch string `json:"default_branch"` + } `json:"repository"` +} diff --git a/remote/gitlab/gitlab/gitlab.go b/remote/gitlab/gitlab.go similarity index 53% rename from remote/gitlab/gitlab/gitlab.go rename to remote/gitlab/gitlab.go index 54268ce2fd..d6846020fc 100644 --- a/remote/gitlab/gitlab/gitlab.go +++ b/remote/gitlab/gitlab.go @@ -9,13 +9,13 @@ import ( "strconv" "strings" - "github.com/drone/drone/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client" - "github.com/drone/drone/Godeps/_workspace/src/github.com/hashicorp/golang-lru" - "github.com/drone/drone/pkg/oauth2" - "github.com/drone/drone/pkg/remote" - "github.com/drone/drone/pkg/token" - common "github.com/drone/drone/pkg/types" - "github.com/drone/drone/pkg/utils/httputil" + "github.com/drone/drone/model" + "github.com/drone/drone/shared/envconfig" + "github.com/drone/drone/shared/httputil" + "github.com/drone/drone/shared/oauth2" + "github.com/drone/drone/shared/token" + + "github.com/Bugagazavr/go-gitlab-client" ) const ( @@ -32,18 +32,14 @@ type Gitlab struct { PrivateMode bool SkipVerify bool Search bool - - cache *lru.Cache } -func init() { - remote.Register("gitlab", NewDriver) -} +func Load(env envconfig.Env) *Gitlab { + config := env.String("REMOTE_CONFIG", "") -func NewDriver(config string) (remote.Remote, error) { url_, err := url.Parse(config) if err != nil { - return nil, err + panic(err) } params := url_.Query() url_.RawQuery = "" @@ -66,40 +62,71 @@ func NewDriver(config string) (remote.Remote, error) { // this is a temp workaround gitlab.Search, _ = strconv.ParseBool(params.Get("search")) - // here we cache permissions to avoid too many api - // calls. this should really be moved outise the - // remote plugin into the app - gitlab.cache, err = lru.New(1028) - return &gitlab, err + return &gitlab } -func (g *Gitlab) Login(token, secret string) (*common.User, error) { - client := NewClient(g.URL, token, g.SkipVerify) - var login, err = client.CurrentUser() +// Login authenticates the session and returns the +// remote user details. +func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { + + var config = &oauth2.Config{ + ClientId: g.Client, + ClientSecret: g.Secret, + Scope: DefaultScope, + AuthURL: fmt.Sprintf("%s/oauth/authorize", g.URL), + TokenURL: fmt.Sprintf("%s/oauth/token", g.URL), + RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(req)), + } + + trans_ := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{InsecureSkipVerify: g.SkipVerify}, + } + + // get the OAuth code + var code = req.FormValue("code") + if len(code) == 0 { + http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther) + return nil, false, nil + } + + var trans = &oauth2.Transport{Config: config, Transport: trans_} + var token_, err = trans.Exchange(code) if err != nil { - return nil, err + return nil, false, fmt.Errorf("Error exchanging token. %s", err) + } + + client := NewClient(g.URL, token_.AccessToken, g.SkipVerify) + login, err := client.CurrentUser() + if err != nil { + return nil, false, err } - user := common.User{} + user := &model.User{} user.Login = login.Username user.Email = login.Email - user.Token = token - user.Secret = secret + user.Token = token_.AccessToken + user.Secret = token_.RefreshToken if strings.HasPrefix(login.AvatarUrl, "http") { user.Avatar = login.AvatarUrl } else { user.Avatar = g.URL + "/" + login.AvatarUrl } - return &user, nil + + return user, true, nil } -// Orgs fetches the organizations for the given user. -func (g *Gitlab) Orgs(u *common.User) ([]string, error) { - return nil, nil +func (g *Gitlab) Auth(token, secret string) (string, error) { + client := NewClient(g.URL, token, g.SkipVerify) + login, err := client.CurrentUser() + if err != nil { + return "", err + } + return login.Username, nil } // Repo fetches the named repository from the remote system. -func (g *Gitlab) Repo(u *common.User, owner, name string) (*common.Repo, error) { +func (g *Gitlab) Repo(u *model.User, owner, name string) (*model.Repo, error) { client := NewClient(g.URL, u.Token, g.SkipVerify) id, err := GetProjectId(g, client, owner, name) if err != nil { @@ -110,7 +137,7 @@ func (g *Gitlab) Repo(u *common.User, owner, name string) (*common.Repo, error) return nil, err } - repo := &common.Repo{} + repo := &model.Repo{} repo.Owner = owner repo.Name = name repo.FullName = repo_.PathWithNamespace @@ -123,22 +150,44 @@ func (g *Gitlab) Repo(u *common.User, owner, name string) (*common.Repo, error) } if g.PrivateMode { - repo.Private = true + repo.IsPrivate = true } else { - repo.Private = !repo_.Public + repo.IsPrivate = !repo_.Public } return repo, err } -// Perm fetches the named repository from the remote system. -func (g *Gitlab) Perm(u *common.User, owner, name string) (*common.Perm, error) { - key := fmt.Sprintf("%s/%s/%s", u.Login, owner, name) - val, ok := g.cache.Get(key) - if ok { - return val.(*common.Perm), nil +// Repos fetches a list of repos from the remote system. +func (g *Gitlab) Repos(u *model.User) ([]*model.RepoLite, error) { + client := NewClient(g.URL, u.Token, g.SkipVerify) + + var repos = []*model.RepoLite{} + + all, err := client.AllProjects() + if err != nil { + return repos, err } + for _, repo := range all { + var parts = strings.Split(repo.PathWithNamespace, "/") + var owner = parts[0] + var name = parts[1] + + repos = append(repos, &model.RepoLite{ + Owner: owner, + Name: name, + FullName: repo.PathWithNamespace, + }) + + // TODO: add repo.AvatarUrl + } + return repos, err +} + +// Perm fetches the named repository from the remote system. +func (g *Gitlab) Perm(u *model.User, owner, name string) (*model.Perm, error) { + client := NewClient(g.URL, u.Token, g.SkipVerify) id, err := GetProjectId(g, client, owner, name) if err != nil { @@ -149,43 +198,42 @@ func (g *Gitlab) Perm(u *common.User, owner, name string) (*common.Perm, error) if err != nil { return nil, err } - m := &common.Perm{} + m := &model.Perm{} m.Admin = IsAdmin(repo) m.Pull = IsRead(repo) m.Push = IsWrite(repo) - g.cache.Add(key, m) return m, nil } // GetScript fetches the build script (.drone.yml) from the remote // repository and returns in string format. -func (g *Gitlab) Script(user *common.User, repo *common.Repo, build *common.Build) ([]byte, []byte, error) { +func (g *Gitlab) Script(user *model.User, repo *model.Repo, build *model.Build) ([]byte, []byte, error) { var client = NewClient(g.URL, user.Token, g.SkipVerify) id, err := GetProjectId(g, client, repo.Owner, repo.Name) if err != nil { return nil, nil, err } - cfg, err := client.RepoRawFile(id, build.Commit.Sha, ".drone.yml") - enc, _ := client.RepoRawFile(id, build.Commit.Sha, ".drone.sec") + cfg, err := client.RepoRawFile(id, build.Commit, ".drone.yml") + enc, _ := client.RepoRawFile(id, build.Commit, ".drone.sec") return cfg, enc, err } // NOTE Currently gitlab doesn't support status for commits and events, // also if we want get MR status in gitlab we need implement a special plugin for gitlab, // gitlab uses API to fetch build status on client side. But for now we skip this. -func (g *Gitlab) Status(u *common.User, repo *common.Repo, b *common.Build) error { +func (g *Gitlab) Status(u *model.User, repo *model.Repo, b *model.Build, link string) error { return nil } // Netrc returns a .netrc file that can be used to clone // private repositories from a remote system. -func (g *Gitlab) Netrc(u *common.User, r *common.Repo) (*common.Netrc, error) { +func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { url_, err := url.Parse(g.URL) if err != nil { return nil, err } - netrc := &common.Netrc{} + netrc := &model.Netrc{} netrc.Machine = url_.Host switch g.CloneMode { @@ -202,7 +250,7 @@ func (g *Gitlab) Netrc(u *common.User, r *common.Repo) (*common.Netrc, error) { // Activate activates a repository by adding a Post-commit hook and // a Public Deploy key, if applicable. -func (g *Gitlab) Activate(user *common.User, repo *common.Repo, k *common.Keypair, link string) error { +func (g *Gitlab) Activate(user *model.User, repo *model.Repo, k *model.Key, link string) error { var client = NewClient(g.URL, user.Token, g.SkipVerify) id, err := GetProjectId(g, client, repo.Owner, repo.Name) if err != nil { @@ -227,7 +275,7 @@ func (g *Gitlab) Activate(user *common.User, repo *common.Repo, k *common.Keypai // Deactivate removes a repository by removing all the post-commit hooks // which are equal to link and removing the SSH deploy key. -func (g *Gitlab) Deactivate(user *common.User, repo *common.Repo, link string) error { +func (g *Gitlab) Deactivate(user *model.User, repo *model.Repo, link string) error { var client = NewClient(g.URL, user.Token, g.SkipVerify) id, err := GetProjectId(g, client, repo.Owner, repo.Name) if err != nil { @@ -239,12 +287,12 @@ func (g *Gitlab) Deactivate(user *common.User, repo *common.Repo, link string) e // ParseHook parses the post-commit hook from the Request body // and returns the required data in a standard format. -func (g *Gitlab) Hook(req *http.Request) (*common.Hook, error) { +func (g *Gitlab) Hook(req *http.Request) (*model.Repo, *model.Build, error) { defer req.Body.Close() var payload, _ = ioutil.ReadAll(req.Body) var parsed, err = gogitlab.ParseHook(payload) if err != nil { - return nil, err + return nil, nil, err } switch parsed.ObjectKind { @@ -253,92 +301,87 @@ func (g *Gitlab) Hook(req *http.Request) (*common.Hook, error) { case "tag_push", "push": return push(parsed, req) default: - return nil, nil + return nil, nil, nil } } -func mergeRequest(parsed *gogitlab.HookPayload, req *http.Request) (*common.Hook, error) { - var hook = new(common.Hook) - hook.Event = "pull_request" - hook.Repo = &common.Repo{} - hook.Repo.Owner = req.FormValue("owner") - hook.Repo.Name = req.FormValue("name") - hook.Repo.FullName = fmt.Sprintf("%s/%s", hook.Repo.Owner, hook.Repo.Name) - hook.Repo.Link = parsed.ObjectAttributes.Target.WebUrl - hook.Repo.Clone = parsed.ObjectAttributes.Target.HttpUrl - hook.Repo.Branch = "master" - - hook.Commit = &common.Commit{} - hook.Commit.Message = parsed.ObjectAttributes.LastCommit.Message - hook.Commit.Sha = parsed.ObjectAttributes.LastCommit.Id - hook.Commit.Remote = parsed.ObjectAttributes.Source.HttpUrl +func mergeRequest(parsed *gogitlab.HookPayload, req *http.Request) (*model.Repo, *model.Build, error) { + + repo := &model.Repo{} + repo.Owner = req.FormValue("owner") + repo.Name = req.FormValue("name") + repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name) + repo.Link = parsed.ObjectAttributes.Target.WebUrl + repo.Clone = parsed.ObjectAttributes.Target.HttpUrl + repo.Branch = "master" + + build := &model.Build{} + build.Event = "pull_request" + build.Message = parsed.ObjectAttributes.LastCommit.Message + build.Commit = parsed.ObjectAttributes.LastCommit.Id + //build.Remote = parsed.ObjectAttributes.Source.HttpUrl if parsed.ObjectAttributes.SourceProjectId == parsed.ObjectAttributes.TargetProjectId { - hook.Commit.Ref = fmt.Sprintf("refs/heads/%s", parsed.ObjectAttributes.SourceBranch) + build.Ref = fmt.Sprintf("refs/heads/%s", parsed.ObjectAttributes.SourceBranch) } else { - hook.Commit.Ref = fmt.Sprintf("refs/merge-requests/%d/head", parsed.ObjectAttributes.IId) + build.Ref = fmt.Sprintf("refs/merge-requests/%d/head", parsed.ObjectAttributes.IId) } - hook.Commit.Branch = parsed.ObjectAttributes.SourceBranch - hook.Commit.Timestamp = parsed.ObjectAttributes.LastCommit.Timestamp + build.Branch = parsed.ObjectAttributes.SourceBranch + // build.Timestamp = parsed.ObjectAttributes.LastCommit.Timestamp - hook.Commit.Author = &common.Author{} - hook.Commit.Author.Login = parsed.ObjectAttributes.LastCommit.Author.Name - hook.Commit.Author.Email = parsed.ObjectAttributes.LastCommit.Author.Email + build.Author = parsed.ObjectAttributes.LastCommit.Author.Name + build.Email = parsed.ObjectAttributes.LastCommit.Author.Email + build.Title = parsed.ObjectAttributes.Title + build.Link = parsed.ObjectAttributes.Url - hook.PullRequest = &common.PullRequest{} - hook.PullRequest.Number = parsed.ObjectAttributes.IId - hook.PullRequest.Title = parsed.ObjectAttributes.Title - hook.PullRequest.Link = parsed.ObjectAttributes.Url - - return hook, nil + return repo, build, nil } -func push(parsed *gogitlab.HookPayload, req *http.Request) (*common.Hook, error) { +func push(parsed *gogitlab.HookPayload, req *http.Request) (*model.Repo, *model.Build, error) { var cloneUrl = parsed.Repository.GitHttpUrl - var hook = new(common.Hook) - hook.Event = "push" - hook.Repo = &common.Repo{} - hook.Repo.Owner = req.FormValue("owner") - hook.Repo.Name = req.FormValue("name") - hook.Repo.Link = parsed.Repository.URL - hook.Repo.Clone = cloneUrl - hook.Repo.Branch = "master" + repo := &model.Repo{} + repo.Owner = req.FormValue("owner") + repo.Name = req.FormValue("name") + repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name) + repo.Link = parsed.Repository.URL + repo.Clone = cloneUrl + repo.Branch = "master" switch parsed.Repository.VisibilityLevel { case 0: - hook.Repo.Private = true + repo.IsPrivate = true case 10: - hook.Repo.Private = true + repo.IsPrivate = true case 20: - hook.Repo.Private = false + repo.IsPrivate = false } - hook.Repo.FullName = fmt.Sprintf("%s/%s", req.FormValue("owner"), req.FormValue("name")) + repo.FullName = fmt.Sprintf("%s/%s", req.FormValue("owner"), req.FormValue("name")) - hook.Commit = &common.Commit{} - hook.Commit.Sha = parsed.After - hook.Commit.Branch = parsed.Branch() - hook.Commit.Ref = parsed.Ref - hook.Commit.Remote = cloneUrl + build := &model.Build{} + build.Event = model.EventPush + build.Commit = parsed.After + build.Branch = parsed.Branch() + build.Ref = parsed.Ref + // hook.Commit.Remote = cloneUrl var head = parsed.Head() - hook.Commit.Message = head.Message - hook.Commit.Timestamp = head.Timestamp - hook.Commit.Author = &common.Author{} + build.Message = head.Message + // build.Timestamp = head.Timestamp // extracts the commit author (ideally email) // from the post-commit hook switch { case head.Author != nil: - hook.Commit.Author.Email = head.Author.Email - hook.Commit.Author.Login = parsed.UserName + build.Email = head.Author.Email + build.Author = parsed.UserName case head.Author == nil: - hook.Commit.Author.Login = parsed.UserName + build.Author = parsed.UserName } - return hook, nil + return repo, build, nil } // ¯\_(ツ)_/¯ diff --git a/remote/gitlab/gitlab/gitlab_test.go b/remote/gitlab/gitlab_test.go similarity index 62% rename from remote/gitlab/gitlab/gitlab_test.go rename to remote/gitlab/gitlab_test.go index e694ab9307..61cdf14ff2 100644 --- a/remote/gitlab/gitlab/gitlab_test.go +++ b/remote/gitlab/gitlab_test.go @@ -2,13 +2,12 @@ package gitlab import ( "bytes" - "fmt" "net/http" "testing" - "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin" - "github.com/drone/drone/pkg/remote/builtin/gitlab/testdata" - "github.com/drone/drone/pkg/types" + "github.com/drone/drone/model" + "github.com/drone/drone/remote/gitlab/testdata" + "github.com/franela/goblin" ) func Test_Gitlab(t *testing.T) { @@ -16,17 +15,17 @@ func Test_Gitlab(t *testing.T) { var server = testdata.NewServer() defer server.Close() - var gitlab, err = NewDriver(server.URL + "?client_id=test&client_secret=test") - if err != nil { - panic(err) - } + env := map[string]string{} + env["REMOTE_CONFIG"] = server.URL + "?client_id=test&client_secret=test" + + gitlab := Load(env) - var user = types.User{ + var user = model.User{ Login: "test_user", Token: "e3b0c44298fc1c149afbf4c8996fb", } - var repo = types.Repo{ + var repo = model.Repo{ Name: "diaspora-client", Owner: "diaspora", } @@ -56,7 +55,6 @@ func Test_Gitlab(t *testing.T) { g.It("Should return repo permissions", func() { perm, err := gitlab.Perm(&user, "diaspora", "diaspora-client") - fmt.Println(gitlab.(*Gitlab), err) g.Assert(err == nil).IsTrue() g.Assert(perm.Admin).Equal(true) g.Assert(perm.Pull).Equal(true) @@ -73,13 +71,13 @@ func Test_Gitlab(t *testing.T) { // Test activate method g.Describe("Activate", func() { g.It("Should be success", func() { - err := gitlab.Activate(&user, &repo, &types.Keypair{}, "http://example.com/api/hook/test/test?access_token=token") + err := gitlab.Activate(&user, &repo, &model.Key{}, "http://example.com/api/hook/test/test?access_token=token") g.Assert(err == nil).IsTrue() }) g.It("Should be failed, when token not given", func() { - err := gitlab.Activate(&user, &repo, &types.Keypair{}, "http://example.com/api/hook/test/test") + err := gitlab.Activate(&user, &repo, &model.Key{}, "http://example.com/api/hook/test/test") g.Assert(err != nil).IsTrue() }) @@ -95,20 +93,20 @@ func Test_Gitlab(t *testing.T) { }) // Test login method - g.Describe("Login", func() { - g.It("Should return user", func() { - user, err := gitlab.Login("valid_token", "") + // g.Describe("Login", func() { + // g.It("Should return user", func() { + // user, err := gitlab.Login("valid_token", "") - g.Assert(err == nil).IsTrue() - g.Assert(user == nil).IsFalse() - }) + // g.Assert(err == nil).IsTrue() + // g.Assert(user == nil).IsFalse() + // }) - g.It("Should return error, when token is invalid", func() { - _, err := gitlab.Login("invalid_token", "") + // g.It("Should return error, when token is invalid", func() { + // _, err := gitlab.Login("invalid_token", "") - g.Assert(err != nil).IsTrue() - }) - }) + // g.Assert(err != nil).IsTrue() + // }) + // }) // Test hook method g.Describe("Hook", func() { @@ -119,14 +117,13 @@ func Test_Gitlab(t *testing.T) { bytes.NewReader(testdata.PushHook), ) - hook, err := gitlab.Hook(req) + repo, build, err := gitlab.Hook(req) g.Assert(err == nil).IsTrue() - g.Assert(hook.Repo.Owner).Equal("diaspora") - g.Assert(hook.Repo.Name).Equal("diaspora-client") - g.Assert(hook.Commit.Ref).Equal("refs/heads/master") + g.Assert(repo.Owner).Equal("diaspora") + g.Assert(repo.Name).Equal("diaspora-client") + g.Assert(build.Ref).Equal("refs/heads/master") - g.Assert(hook.PullRequest == nil).IsTrue() }) g.It("Should parse tag push hook", func() { @@ -136,14 +133,13 @@ func Test_Gitlab(t *testing.T) { bytes.NewReader(testdata.TagHook), ) - hook, err := gitlab.Hook(req) + repo, build, err := gitlab.Hook(req) g.Assert(err == nil).IsTrue() - g.Assert(hook.Repo.Owner).Equal("diaspora") - g.Assert(hook.Repo.Name).Equal("diaspora-client") - g.Assert(hook.Commit.Ref).Equal("refs/tags/v1.0.0") + g.Assert(repo.Owner).Equal("diaspora") + g.Assert(repo.Name).Equal("diaspora-client") + g.Assert(build.Ref).Equal("refs/tags/v1.0.0") - g.Assert(hook.PullRequest == nil).IsTrue() }) g.It("Should parse merge request hook", func() { @@ -153,14 +149,13 @@ func Test_Gitlab(t *testing.T) { bytes.NewReader(testdata.MergeRequestHook), ) - hook, err := gitlab.Hook(req) + repo, build, err := gitlab.Hook(req) g.Assert(err == nil).IsTrue() - g.Assert(hook.Repo.Owner).Equal("diaspora") - g.Assert(hook.Repo.Name).Equal("diaspora-client") + g.Assert(repo.Owner).Equal("diaspora") + g.Assert(repo.Name).Equal("diaspora-client") - g.Assert(hook.PullRequest.Number).Equal(1) - g.Assert(hook.PullRequest.Title).Equal("MS-Viewport") + g.Assert(build.Title).Equal("MS-Viewport") }) }) }) diff --git a/remote/gitlab/gitlab/helper.go b/remote/gitlab/helper.go similarity index 96% rename from remote/gitlab/gitlab/helper.go rename to remote/gitlab/helper.go index adc448d26d..25b7f1d5c5 100644 --- a/remote/gitlab/gitlab/helper.go +++ b/remote/gitlab/helper.go @@ -5,7 +5,7 @@ import ( "net/url" "strconv" - "github.com/drone/drone/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client" + "github.com/Bugagazavr/go-gitlab-client" ) // NewClient is a helper function that returns a new GitHub diff --git a/remote/gitlab/gitlab/testdata/hooks.go b/remote/gitlab/testdata/hooks.go similarity index 100% rename from remote/gitlab/gitlab/testdata/hooks.go rename to remote/gitlab/testdata/hooks.go diff --git a/remote/gitlab/gitlab/testdata/oauth.go b/remote/gitlab/testdata/oauth.go similarity index 100% rename from remote/gitlab/gitlab/testdata/oauth.go rename to remote/gitlab/testdata/oauth.go diff --git a/remote/gitlab/gitlab/testdata/projects.go b/remote/gitlab/testdata/projects.go similarity index 100% rename from remote/gitlab/gitlab/testdata/projects.go rename to remote/gitlab/testdata/projects.go diff --git a/remote/gitlab/gitlab/testdata/testdata.go b/remote/gitlab/testdata/testdata.go similarity index 100% rename from remote/gitlab/gitlab/testdata/testdata.go rename to remote/gitlab/testdata/testdata.go diff --git a/remote/gitlab/gitlab/testdata/users.go b/remote/gitlab/testdata/users.go similarity index 100% rename from remote/gitlab/gitlab/testdata/users.go rename to remote/gitlab/testdata/users.go diff --git a/remote/remote.go b/remote/remote.go index e1d4cd39e3..5f26c9ebf6 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -3,91 +3,70 @@ package remote import ( "net/http" - "github.com/drone/drone/pkg/oauth2" - "github.com/drone/drone/pkg/types" + "github.com/drone/drone/model" + "github.com/drone/drone/remote/github" + "github.com/drone/drone/remote/gitlab" + "github.com/drone/drone/shared/envconfig" - log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus" + log "github.com/Sirupsen/logrus" ) -var drivers = make(map[string]DriverFunc) +func Load(env envconfig.Env) Remote { + driver := env.Get("REMOTE_DRIVER") -// Register makes a remote driver available by the provided name. -// If Register is called twice with the same name or if driver is nil, -// it panics. -func Register(name string, driver DriverFunc) { - if driver == nil { - panic("remote: Register driver is nil") - } - if _, dup := drivers[name]; dup { - panic("remote: Register called twice for driver " + name) - } - drivers[name] = driver -} + switch driver { + case "github": + return github.Load(env) + case "gitlab": + return gitlab.Load(env) -// DriverFunc returns a new connection to the remote. -// Config is a struct, with base remote configuration. -type DriverFunc func(config string) (Remote, error) - -// New creates a new remote connection. -func New(driver, config string) (Remote, error) { - fn, ok := drivers[driver] - if !ok { - log.Fatalf("remote: unknown driver %q", driver) + default: + log.Fatalf("unknown remote driver %s", driver) } - log.Infof("remote: loading driver %s", driver) - log.Infof("remote: loading config %s", config) - return fn(config) + + return nil } type Remote interface { // Login authenticates the session and returns the // remote user details. - Login(token, secret string) (*types.User, error) + Login(w http.ResponseWriter, r *http.Request) (*model.User, bool, error) - // Orgs fetches the organizations for the given user. - Orgs(u *types.User) ([]string, error) + // Auth authenticates the session and returns the remote user + // login for the given token and secret + Auth(token, secret string) (string, error) // Repo fetches the named repository from the remote system. - Repo(u *types.User, owner, repo string) (*types.Repo, error) + Repo(u *model.User, owner, repo string) (*model.Repo, error) + + // Repos fetches a list of repos from the remote system. + Repos(u *model.User) ([]*model.RepoLite, error) // Perm fetches the named repository permissions from // the remote system for the specified user. - Perm(u *types.User, owner, repo string) (*types.Perm, error) + Perm(u *model.User, owner, repo string) (*model.Perm, error) // Script fetches the build script (.drone.yml) from the remote // repository and returns in string format. - Script(u *types.User, r *types.Repo, b *types.Build) ([]byte, []byte, error) + Script(u *model.User, r *model.Repo, b *model.Build) ([]byte, []byte, error) // Status sends the commit status to the remote system. // An example would be the GitHub pull request status. - Status(u *types.User, r *types.Repo, b *types.Build) error + Status(u *model.User, r *model.Repo, b *model.Build, link string) error // Netrc returns a .netrc file that can be used to clone // private repositories from a remote system. - Netrc(u *types.User, r *types.Repo) (*types.Netrc, error) + Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) // Activate activates a repository by creating the post-commit hook and // adding the SSH deploy key, if applicable. - Activate(u *types.User, r *types.Repo, k *types.Keypair, link string) error + Activate(u *model.User, r *model.Repo, k *model.Key, link string) error // Deactivate removes a repository by removing all the post-commit hooks // which are equal to link and removing the SSH deploy key. - Deactivate(u *types.User, r *types.Repo, link string) error + Deactivate(u *model.User, r *model.Repo, link string) error // Hook parses the post-commit hook from the Request body // and returns the required data in a standard format. - Hook(r *http.Request) (*types.Hook, error) - - // Oauth2Transport - Oauth2Transport(r *http.Request) *oauth2.Transport - - // GetOrgs returns all allowed organizations for remote. - GetOrgs() []string - - // GetOpen returns boolean field with enabled or disabled - // registration. - GetOpen() bool - - // Default scope for remote - Scope() string + Hook(r *http.Request) (*model.Repo, *model.Build, error) } diff --git a/router/middleware/context/context.go b/router/middleware/context/context.go new file mode 100644 index 0000000000..e7b0865885 --- /dev/null +++ b/router/middleware/context/context.go @@ -0,0 +1,42 @@ +package context + +import ( + "database/sql" + + "github.com/drone/drone/engine" + "github.com/drone/drone/remote" + "github.com/gin-gonic/gin" +) + +func SetDatabase(db *sql.DB) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("database", db) + c.Next() + } +} + +func Database(c *gin.Context) *sql.DB { + return c.MustGet("database").(*sql.DB) +} + +func SetRemote(remote remote.Remote) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("remote", remote) + c.Next() + } +} + +func Remote(c *gin.Context) remote.Remote { + return c.MustGet("remote").(remote.Remote) +} + +func SetEngine(engine engine.Engine) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("engine", engine) + c.Next() + } +} + +func Engine(c *gin.Context) engine.Engine { + return c.MustGet("engine").(engine.Engine) +} diff --git a/router/middleware/header/header.go b/router/middleware/header/header.go new file mode 100644 index 0000000000..9331676bd6 --- /dev/null +++ b/router/middleware/header/header.go @@ -0,0 +1,40 @@ +package header + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func SetHeaders() gin.HandlerFunc { + return func(c *gin.Context) { + + c.Writer.Header().Add("Access-Control-Allow-Origin", "*") + c.Writer.Header().Add("X-Frame-Options", "DENY") + c.Writer.Header().Add("X-Content-Type-Options", "nosniff") + c.Writer.Header().Add("X-XSS-Protection", "1; mode=block") + c.Writer.Header().Add("Cache-Control", "no-cache") + c.Writer.Header().Add("Cache-Control", "no-store") + c.Writer.Header().Add("Cache-Control", "max-age=0") + c.Writer.Header().Add("Cache-Control", "must-revalidate") + c.Writer.Header().Add("Cache-Control", "value") + c.Writer.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) + c.Writer.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") + //c.Writer.Header().Set("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com") + if c.Request.TLS != nil { + c.Writer.Header().Add("Strict-Transport-Security", "max-age=31536000") + } + + if c.Request.Method == "OPTIONS" { + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization") + c.Writer.Header().Set("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") + c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.WriteHeader(200) + return + } + + c.Next() + } +} diff --git a/router/middleware/session/repo.go b/router/middleware/session/repo.go new file mode 100644 index 0000000000..18f69836db --- /dev/null +++ b/router/middleware/session/repo.go @@ -0,0 +1,236 @@ +package session + +import ( + "fmt" + "net/http" + + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + + log "github.com/Sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/hashicorp/golang-lru" +) + +var cache *lru.Cache + +func init() { + var err error + cache, err = lru.New(1028) + if err != nil { + panic(err) + } +} + +func Repo(c *gin.Context) *model.Repo { + v, ok := c.Get("repo") + if !ok { + return nil + } + u, ok := v.(*model.Repo) + if !ok { + return nil + } + return u +} + +func SetRepo() gin.HandlerFunc { + return func(c *gin.Context) { + var ( + owner = c.Param("owner") + name = c.Param("name") + ) + + db := context.Database(c) + user := User(c) + repo, err := model.GetRepoName(db, owner, name) + if err == nil { + c.Set("repo", repo) + c.Next() + return + } + + // if the user is not nil, check the remote system + // to see if the repository actually exists. If yes, + // we can prompt the user to add. + if user != nil { + remote := context.Remote(c) + repo, _ = remote.Repo(user, owner, name) + } + + data := gin.H{ + "User": user, + "Repo": repo, + } + + // if we found a repository, we should display a page + // to the user allowing them to activate. + if repo != nil && len(repo.FullName) != 0 { + c.HTML(http.StatusNotFound, "repo_activate.html", data) + } else { + c.HTML(http.StatusNotFound, "404.html", data) + } + + c.Abort() + } +} + +func Perm(c *gin.Context) *model.Perm { + v, ok := c.Get("perm") + if !ok { + return nil + } + u, ok := v.(*model.Perm) + if !ok { + return nil + } + return u +} + +func SetPerm() gin.HandlerFunc { + return func(c *gin.Context) { + user := User(c) + repo := Repo(c) + remote := context.Remote(c) + perm := &model.Perm{} + + if user != nil { + // attempt to get the permissions from a local cache + // just to avoid excess API calls to GitHub + key := fmt.Sprintf("%d.%d", user.ID, repo.ID) + val, ok := cache.Get(key) + if ok { + c.Set("perm", val.(*model.Perm)) + c.Next() + + log.Debugf("%s using cached %+v permission to %s", + user.Login, val, repo.FullName) + return + } + } + + switch { + // if the user is not authenticated, and the + // repository is private, the user has NO permission + // to view the repository. + case user == nil && repo.IsPrivate == true: + perm.Pull = false + perm.Push = false + perm.Admin = false + + // if the user is not authenticated, but the repository + // is public, the user has pull-rights only. + case user == nil && repo.IsPrivate == false: + perm.Pull = true + perm.Push = false + perm.Admin = false + + case user.Admin: + perm.Pull = true + perm.Push = true + perm.Admin = true + + // otherwise if the user is authenticated we should + // check the remote system to get the users permissiosn. + default: + var err error + perm, err = remote.Perm(user, repo.Owner, repo.Name) + if err != nil { + perm.Pull = false + perm.Push = false + perm.Admin = false + + // debug + log.Errorf("Error fetching permission for %s %s", + user.Login, repo.FullName) + } + // if we couldn't fetch permissions, but the repository + // is public, we should grant the user pull access. + if err != nil && repo.IsPrivate == false { + perm.Pull = true + } + } + + if user != nil { + + // cache the updated repository permissions to + // prevent un-necessary GitHub API requests. + key := fmt.Sprintf("%d.%d", user.ID, repo.ID) + cache.Add(key, perm) + + // debug + log.Debugf("%s granted %+v permission to %s", + user.Login, perm, repo.FullName) + + } else { + log.Debugf("Guest granted %+v to %s", perm, repo.FullName) + } + + c.Set("perm", perm) + c.Next() + } +} + +func MustPull(c *gin.Context) { + user := User(c) + repo := Repo(c) + perm := Perm(c) + + if perm.Pull { + c.Next() + return + } + + // if the user doesn't have pull permission to the + // repository we display a 404 error to avoid leaking + // repository information. + c.HTML(http.StatusNotFound, "404.html", gin.H{ + "User": user, + "Repo": repo, + "Perm": perm, + }) + + c.Abort() +} + +func MustPush(c *gin.Context) { + user := User(c) + repo := Repo(c) + perm := Perm(c) + + // if the user has push access, immediately proceed + // the middleware execution chain. + if perm.Push { + c.Next() + return + } + + data := gin.H{ + "User": user, + "Repo": repo, + "Perm": perm, + } + + // if the user has pull access we should tell them + // the operation is not authorized. Otherwise we should + // give a 404 to avoid leaking information. + if !perm.Pull { + c.HTML(http.StatusNotFound, "404.html", data) + } else { + c.HTML(http.StatusUnauthorized, "401.html", data) + } + + // debugging + if user != nil { + log.Debugf("%s denied write access to %s", + user.Login, c.Request.URL.Path) + + } else { + log.Debugf("Guest denied write access to %s %s", + c.Request.Method, + c.Request.URL.Path, + ) + } + + c.Abort() +} diff --git a/router/middleware/session/user.go b/router/middleware/session/user.go new file mode 100644 index 0000000000..4d403d0eff --- /dev/null +++ b/router/middleware/session/user.go @@ -0,0 +1,98 @@ +package session + +import ( + "net/http" + + "github.com/drone/drone/model" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/shared/token" + + "github.com/gin-gonic/gin" +) + +func User(c *gin.Context) *model.User { + v, ok := c.Get("user") + if !ok { + return nil + } + u, ok := v.(*model.User) + if !ok { + return nil + } + return u +} + +func Token(c *gin.Context) *token.Token { + v, ok := c.Get("token") + if !ok { + return nil + } + u, ok := v.(*token.Token) + if !ok { + return nil + } + return u +} + +func SetUser() gin.HandlerFunc { + return func(c *gin.Context) { + var user *model.User + + t, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { + var db = context.Database(c) + var err error + user, err = model.GetUserLogin(db, t.Text) + return user.Hash, err + }) + if err == nil { + c.Set("user", user) + + // if this is a session token (ie not the API token) + // this means the user is accessing with a web browser, + // so we should implement CSRF protection measures. + if t.Kind == token.SessToken { + err = token.CheckCsrf(c.Request, func(t *token.Token) (string, error) { + return user.Hash, nil + }) + // if csrf token validation fails, exit immediately + // with a not authorized error. + if err != nil { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + } + } + c.Next() + } +} + +func MustAdmin() gin.HandlerFunc { + return func(c *gin.Context) { + user := User(c) + switch { + case user == nil: + c.AbortWithStatus(http.StatusUnauthorized) + // c.HTML(http.StatusUnauthorized, "401.html", gin.H{}) + case user.Admin == false: + c.AbortWithStatus(http.StatusForbidden) + // c.HTML(http.StatusForbidden, "401.html", gin.H{}) + default: + c.Next() + } + + } +} + +func MustUser() gin.HandlerFunc { + return func(c *gin.Context) { + user := User(c) + switch { + case user == nil: + c.AbortWithStatus(http.StatusUnauthorized) + // c.HTML(http.StatusUnauthorized, "401.html", gin.H{}) + default: + c.Next() + } + + } +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000000..5962a334ec --- /dev/null +++ b/router/router.go @@ -0,0 +1,179 @@ +package router + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "github.com/drone/drone/controller" + "github.com/drone/drone/router/middleware/header" + "github.com/drone/drone/router/middleware/session" + "github.com/drone/drone/static" + "github.com/drone/drone/template" +) + +func Load(middleware ...gin.HandlerFunc) http.Handler { + e := gin.Default() + e.SetHTMLTemplate(template.Load()) + e.StaticFS("/static", static.FileSystem()) + + e.Use(header.SetHeaders()) + e.Use(middleware...) + e.Use(session.SetUser()) + + e.GET("/", controller.ShowIndex) + e.GET("/login", controller.ShowLogin) + e.GET("/logout", controller.GetLogout) + + settings := e.Group("/settings") + { + settings.Use(session.MustUser()) + settings.GET("/profile", controller.ShowUser) + settings.GET("/people", session.MustAdmin(), controller.ShowUsers) + settings.GET("/nodes", session.MustAdmin(), controller.ShowNodes) + } + repo := e.Group("/repos/:owner/:name") + { + repo.Use(session.SetRepo()) + repo.Use(session.SetPerm()) + repo.Use(session.MustPull) + + repo.GET("", controller.ShowRepo) + repo.GET("/builds/:number", controller.ShowBuild) + repo.GET("/builds/:number/:job", controller.ShowBuild) + repo_settings := repo.Group("/settings") + { + repo_settings.Use(session.MustPush) + repo_settings.GET("", controller.ShowRepoConf) + repo_settings.GET("/:action", controller.ShowRepoConf) + } + } + + user := e.Group("/api/user") + { + user.Use(session.MustUser()) + user.GET("", controller.GetSelf) + user.GET("/feed", controller.GetFeed) + user.GET("/repos", controller.GetRepos) + user.POST("/token", controller.PostToken) + user.GET("/repos/remote", controller.GetRemoteRepos) + } + + users := e.Group("/api/users") + { + users.Use(session.MustAdmin()) + users.GET("", controller.GetUsers) + users.POST("", controller.PostUser) + users.GET("/:login", controller.GetUser) + users.PATCH("/:login", controller.PatchUser) + users.DELETE("/:login", controller.DeleteUser) + } + + nodes := e.Group("/api/nodes") + { + nodes.Use(session.MustAdmin()) + nodes.GET("", controller.GetNodes) + nodes.POST("", controller.PostNode) + nodes.DELETE("/:node", controller.DeleteNode) + } + + repos := e.Group("/api/repos/:owner/:name") + { + repos.POST("", controller.PostRepo) + + repo := repos.Group("") + { + repo.Use(session.SetRepo()) + repo.Use(session.SetPerm()) + repo.Use(session.MustPull) + + repo.GET("", controller.GetRepo) + repo.GET("/key", controller.GetRepoKey) + repo.GET("/builds", controller.GetBuilds) + repo.GET("/builds/:number", controller.GetBuild) + repo.GET("/logs/:number/:job", controller.GetBuildLogs) + + // requires authenticated user + repo.POST("/starred", session.MustUser(), controller.PostStar) + repo.DELETE("/starred", session.MustUser(), controller.DeleteStar) + repo.POST("/encrypt", session.MustUser(), controller.PostSecure) + + // requires push permissions + repo.PATCH("", session.MustPush, controller.PatchRepo) + repo.DELETE("", session.MustPush, controller.DeleteRepo) + + repo.POST("/builds/:number", session.MustPush, controller.PostBuild) + // repo.DELETE("/builds/:number", MustPush(), controller.DeleteBuild) + } + } + + badges := e.Group("/api/badges/:owner/:name") + { + badges.GET("/status.svg", controller.GetBadge) + badges.GET("/cc.xml", controller.GetCC) + } + + hook := e.Group("/hook") + { + hook.POST("", controller.PostHook) + } + + stream := e.Group("/api/stream") + { + stream.Use(session.SetRepo()) + stream.Use(session.SetPerm()) + stream.Use(session.MustPull) + stream.GET("/:owner/:name", controller.GetRepoEvents) + stream.GET("/:owner/:name/:build/:number", controller.GetStream) + } + + auth := e.Group("/authorize") + { + auth.GET("", controller.GetLogin) + auth.POST("", controller.GetLogin) + auth.POST("/token", controller.GetLoginToken) + } + + gitlab := e.Group("/api/gitlab/:owner/:name") + { + gitlab.Use(session.SetRepo()) + gitlab.GET("/commits/:sha", controller.GetCommit) + gitlab.GET("/pulls/:number", controller.GetPullRequest) + + redirects := gitlab.Group("/redirect") + { + redirects.GET("/commits/:sha", controller.RedirectSha) + redirects.GET("/pulls/:number", controller.RedirectPullRequest) + } + } + + return normalize(e) +} + +// normalize is a helper function to work around the following +// issue with gin. https://github.com/gin-gonic/gin/issues/388 +func normalize(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + parts := strings.Split(r.URL.Path, "/")[1:] + switch parts[0] { + case "settings", "api", "login", "logout", "", "authorize", "hook", "static": + // no-op + default: + + if len(parts) > 2 && parts[2] != "settings" { + parts = append(parts[:2], append([]string{"builds"}, parts[2:]...)...) + } + + // prefix the URL with /repo so that it + // can be effectively routed. + parts = append([]string{"", "repos"}, parts...) + + // reconstruct the path + r.URL.Path = strings.Join(parts, "/") + } + + h.ServeHTTP(w, r) + }) +} diff --git a/shared/crypto/crypto.go b/shared/crypto/crypto.go new file mode 100644 index 0000000000..76fa6b8948 --- /dev/null +++ b/shared/crypto/crypto.go @@ -0,0 +1,118 @@ +package crypto + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "io" + + "code.google.com/p/go.crypto/ssh" + "github.com/square/go-jose" +) + +const ( + RSA_BITS = 2048 // Default number of bits in an RSA key + RSA_BITS_MIN = 768 // Minimum number of bits in an RSA key +) + +// standard characters allowed in token string. +var chars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + +// default token length +var length = 32 + +// Rand generates a 32-bit random string. +func Rand() string { + b := make([]byte, length) + r := make([]byte, length+(length/4)) // storage for random bytes. + clen := byte(len(chars)) + maxrb := byte(256 - (256 % len(chars))) + i := 0 + for { + io.ReadFull(rand.Reader, r) + for _, c := range r { + if c >= maxrb { + // Skip this number to avoid modulo bias. + continue + } + b[i] = chars[c%clen] + i++ + if i == length { + return string(b) + } + } + } +} + +// helper function to generate an RSA Private Key. +func GeneratePrivateKey() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, RSA_BITS) +} + +// helper function that marshalls an RSA Public Key to an SSH +// .authorized_keys format +func MarshalPublicKey(public *rsa.PublicKey) []byte { + private, err := ssh.NewPublicKey(public) + if err != nil { + return []byte{} + } + + return ssh.MarshalAuthorizedKey(private) +} + +// helper function that marshalls an RSA Private Key to +// a PEM encoded file. +func MarshalPrivateKey(private *rsa.PrivateKey) []byte { + marshaled := x509.MarshalPKCS1PrivateKey(private) + encoded := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Headers: nil, Bytes: marshaled}) + return encoded +} + +// UnmarshalPrivateKey is a helper function that unmarshals a PEM +// bytes to an RSA Private Key +func UnmarshalPrivateKey(private []byte) *rsa.PrivateKey { + decoded, _ := pem.Decode(private) + parsed, err := x509.ParsePKCS1PrivateKey(decoded.Bytes) + if err != nil { + return nil + } + return parsed +} + +// Encrypt encrypts a secret string. +func Encrypt(in, privKey string) (string, error) { + rsaPrivKey, err := decodePrivateKey(privKey) + if err != nil { + return "", err + } + + return encrypt(in, &rsaPrivKey.PublicKey) +} + +// decodePrivateKey is a helper function that unmarshals a PEM +// bytes to an RSA Private Key +func decodePrivateKey(privateKey string) (*rsa.PrivateKey, error) { + derBlock, _ := pem.Decode([]byte(privateKey)) + return x509.ParsePKCS1PrivateKey(derBlock.Bytes) +} + +// encrypt encrypts a plaintext variable using JOSE with +// RSA_OAEP and A128GCM algorithms. +func encrypt(text string, pubKey *rsa.PublicKey) (string, error) { + var encrypted string + var plaintext = []byte(text) + + // Creates a new encrypter using defaults + encrypter, err := jose.NewEncrypter(jose.RSA_OAEP, jose.A128GCM, pubKey) + if err != nil { + return encrypted, err + } + // Encrypts the plaintext value and serializes + // as a JOSE string. + object, err := encrypter.Encrypt(plaintext) + if err != nil { + return encrypted, err + } + return object.CompactSerialize() +} diff --git a/yaml/secure/secure_test.go b/shared/crypto/crypto_test.go similarity index 86% rename from yaml/secure/secure_test.go rename to shared/crypto/crypto_test.go index 3e08f99baf..35b7e488d1 100644 --- a/yaml/secure/secure_test.go +++ b/shared/crypto/crypto_test.go @@ -1,13 +1,25 @@ -package secure +package crypto import ( "testing" - "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/square/go-jose" + "github.com/franela/goblin" + "github.com/square/go-jose" ) -func Test_Secure(t *testing.T) { +func TestKeys(t *testing.T) { + + g := goblin.Goblin(t) + g.Describe("Generate Key", func() { + + g.It("Generates a private key", func() { + _, err := GeneratePrivateKey() + g.Assert(err == nil).IsTrue() + }) + }) +} + +func Test_Encrypt(t *testing.T) { g := goblin.Goblin(t) g.Describe("Secure", func() { diff --git a/shared/crypto/sshutil/sshutil.go b/shared/crypto/sshutil/sshutil.go deleted file mode 100644 index 4796c6fbae..0000000000 --- a/shared/crypto/sshutil/sshutil.go +++ /dev/null @@ -1,72 +0,0 @@ -package sshutil - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "hash" - - "github.com/drone/drone/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh" -) - -const ( - RSA_BITS = 2048 // Default number of bits in an RSA key - RSA_BITS_MIN = 768 // Minimum number of bits in an RSA key -) - -// helper function to generate an RSA Private Key. -func GeneratePrivateKey() (*rsa.PrivateKey, error) { - return rsa.GenerateKey(rand.Reader, RSA_BITS) -} - -// helper function that marshalls an RSA Public Key to an SSH -// .authorized_keys format -func MarshalPublicKey(pubkey *rsa.PublicKey) []byte { - pk, err := ssh.NewPublicKey(pubkey) - if err != nil { - return []byte{} - } - - return ssh.MarshalAuthorizedKey(pk) -} - -// helper function that marshalls an RSA Private Key to -// a PEM encoded file. -func MarshalPrivateKey(privkey *rsa.PrivateKey) []byte { - privateKeyMarshaled := x509.MarshalPKCS1PrivateKey(privkey) - privateKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Headers: nil, Bytes: privateKeyMarshaled}) - return privateKeyPEM -} - -// UnMarshalPrivateKey is a helper function that unmarshals a PEM -// bytes to an RSA Private Key -func UnMarshalPrivateKey(privateKeyPEM []byte) *rsa.PrivateKey { - derBlock, _ := pem.Decode(privateKeyPEM) - privateKey, err := x509.ParsePKCS1PrivateKey(derBlock.Bytes) - - if err != nil { - return nil - } - return privateKey -} - -// Encrypt is helper function to encrypt a plain-text string using -// an RSA public key. -func Encrypt(hash hash.Hash, pubkey *rsa.PublicKey, msg string) (string, error) { - src, err := rsa.EncryptOAEP(hash, rand.Reader, pubkey, []byte(msg), nil) - return base64.RawURLEncoding.EncodeToString(src), err -} - -// Decrypt is helper function to encrypt a plain-text string using -// an RSA public key. -func Decrypt(hash hash.Hash, privkey *rsa.PrivateKey, secret string) (string, error) { - decoded, err := base64.RawURLEncoding.DecodeString(secret) - if err != nil { - return "", err - } - - out, err := rsa.DecryptOAEP(hash, rand.Reader, privkey, decoded, nil) - return string(out), err -} diff --git a/shared/crypto/sshutil/sshutil_test.go b/shared/crypto/sshutil/sshutil_test.go deleted file mode 100644 index 051f4748f8..0000000000 --- a/shared/crypto/sshutil/sshutil_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package sshutil - -import ( - "crypto/sha256" - "testing" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin" -) - -func TestSSHUtil(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("sshutil", func() { - var encrypted, testMsg string - - privkey, err := GeneratePrivateKey() - g.Assert(err == nil).IsTrue() - pubkey := privkey.PublicKey - sha256 := sha256.New() - testMsg = "foo=bar" - - g.Before(func() { - encrypted, err = Encrypt(sha256, &pubkey, testMsg) - g.Assert(err == nil).IsTrue() - }) - - g.It("Can decrypt encrypted msg", func() { - decrypted, err := Decrypt(sha256, privkey, encrypted) - g.Assert(err == nil).IsTrue() - g.Assert(decrypted == testMsg).IsTrue() - }) - - g.It("Unmarshals private key from PEM block", func() { - privateKeyPEM := MarshalPrivateKey(privkey) - privateKey := UnMarshalPrivateKey(privateKeyPEM) - - g.Assert(privateKey.PublicKey.E == pubkey.E).IsTrue() - }) - }) -} diff --git a/shared/database/database.go b/shared/database/database.go new file mode 100644 index 0000000000..6b74d34e73 --- /dev/null +++ b/shared/database/database.go @@ -0,0 +1,51 @@ +package database + +//go:generate go-bindata -pkg database -o database_gen.go sqlite3/ mysql/ postgres/ + +import ( + "database/sql" + + "github.com/drone/drone/shared/envconfig" + + log "github.com/Sirupsen/logrus" + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" + "github.com/rubenv/sql-migrate" +) + +func Load(env envconfig.Env) *sql.DB { + var ( + driver = env.String("DATABASE_DRIVER", "sqlite3") + config = env.String("DATABASE_CONFIG", "drone.sqlite") + ) + + log.Infof("using database driver %s", driver) + log.Infof("using database config %s", config) + + return Open(driver, config) +} + +// Open opens a database connection, runs the database migrations, and returns +// the database connection. Any errors connecting to the database or executing +// migrations will cause the application to exit. +func Open(driver, config string) *sql.DB { + var db, err = sql.Open(driver, config) + if err != nil { + log.Errorln(err) + log.Fatalln("database connection failed") + } + + var migrations = &migrate.AssetMigrationSource{ + Asset: Asset, + AssetDir: AssetDir, + Dir: driver, + } + + _, err = migrate.Exec(db, driver, migrations, migrate.Up) + if err != nil { + log.Errorln(err) + log.Fatalln("migration failed") + } + return db +} diff --git a/shared/database/mysql/1_init.sql b/shared/database/mysql/1_init.sql new file mode 100644 index 0000000000..035c204fa4 --- /dev/null +++ b/shared/database/mysql/1_init.sql @@ -0,0 +1,132 @@ +-- +migrate Up + +CREATE TABLE users ( + user_id INTEGER PRIMARY KEY AUTO_INCREMENT +,user_login VARCHAR(500) +,user_token VARCHAR(500) +,user_secret VARCHAR(500) +,user_email VARCHAR(500) +,user_avatar VARCHAR(500) +,user_active BOOLEAN +,user_admin BOOLEAN +,user_hash VARCHAR(500) + +,UNIQUE(user_login) +); + +CREATE TABLE repos ( + repo_id INTEGER PRIMARY KEY AUTO_INCREMENT +,repo_user_id INTEGER +,repo_owner VARCHAR(500) +,repo_name VARCHAR(500) +,repo_full_name VARCHAR(1000) +,repo_avatar VARCHAR(500) +,repo_link VARCHAR(1000) +,repo_clone VARCHAR(1000) +,repo_branch VARCHAR(500) +,repo_timeout INTEGER +,repo_private BOOLEAN +,repo_trusted BOOLEAN +,repo_allow_pr BOOLEAN +,repo_allow_push BOOLEAN +,repo_allow_deploys BOOLEAN +,repo_allow_tags BOOLEAN +,repo_hash VARCHAR(500) + +,UNIQUE(repo_owner, repo_name) +); + +CREATE TABLE stars ( + star_id INTEGER PRIMARY KEY AUTO_INCREMENT +,star_repo_id INTEGER +,star_user_id INTEGER + +,UNIQUE(star_repo_id, star_user_id) +); + +CREATE INDEX ix_star_user ON builds (star_user_id); + +CREATE TABLE keys ( + key_id INTEGER PRIMARY KEY AUTO_INCREMENT +,key_repo_id INTEGER +,key_public MEDIUMBLOB +,key_private MEDIUMBLOB + +,UNIQUE(key_repo_id) +); + +CREATE TABLE builds ( + build_id INTEGER PRIMARY KEY AUTO_INCREMENT +,build_repo_id INTEGER +,build_number INTEGER +,build_event VARCHAR(500) +,build_status VARCHAR(500) +,build_created INTEGER +,build_started INTEGER +,build_finished INTEGER +,build_commit VARCHAR(500) +,build_branch VARCHAR(500) +,build_ref VARCHAR(500) +,build_refspec VARCHAR(1000) +,build_remote VARCHAR(500) +,build_title VARCHAR(1000) +,build_message VARCHAR(2000) +,build_timestamp INTEGER +,build_author VARCHAR(500) +,build_avatar VARCHAR(1000) +,build_email VARCHAR(500) +,build_link VARCHAR(1000) + +,UNIQUE(build_number, build_repo_id) +); + +CREATE INDEX ix_build_repo ON builds (build_repo_id); + +CREATE TABLE jobs ( + job_id INTEGER PRIMARY KEY AUTO_INCREMENT +,job_node_id INTEGER +,job_build_id INTEGER +,job_number INTEGER +,job_status VARCHAR(500) +,job_exit_code INTEGER +,job_started INTEGER +,job_finished INTEGER +,job_environment VARCHAR(2000) + +,UNIQUE(job_build_id, job_number) +); + +CREATE INDEX ix_job_build ON jobs (job_build_id); +CREATE INDEX ix_job_node ON jobs (job_node_id); + +CREATE TABLE IF NOT EXISTS logs ( + log_id INTEGER PRIMARY KEY AUTO_INCREMENT +,log_job_id INTEGER +,log_data MEDIUMBLOB + +,UNIQUE(log_job_id) +); + +CREATE TABLE IF NOT EXISTS nodes ( + node_id INTEGER PRIMARY KEY AUTOINCREMENT +,node_addr VARCHAR(1024) +,node_arch VARCHAR(50) +,node_cert MEDIUMBLOB +,node_key MEDIUMBLOB +,node_ca MEDIUMBLOB +); + + +INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', ''); +INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', ''); + +-- +migrate Down + +DROP TABLE nodes; +DROP TABLE logs; +DROP TABLE jobs; +DROP TABLE builds; +DROP TABLE keys; +DROP TABLE stars; +DROP TABLE repos; +DROP TABLE users; \ No newline at end of file diff --git a/shared/database/postgres/1_init.sql b/shared/database/postgres/1_init.sql new file mode 100644 index 0000000000..6109390740 --- /dev/null +++ b/shared/database/postgres/1_init.sql @@ -0,0 +1,132 @@ +-- +migrate Up + +CREATE TABLE users ( + user_id SERIAL PRIMARY KEY +,user_login VARCHAR(500) +,user_token VARCHAR(500) +,user_secret VARCHAR(500) +,user_email VARCHAR(500) +,user_avatar VARCHAR(500) +,user_active BOOLEAN +,user_admin BOOLEAN +,user_hash VARCHAR(500) + +,UNIQUE(user_login) +); + +CREATE TABLE repos ( + repo_id SERIAL PRIMARY KEY +,repo_user_id INTEGER +,repo_owner VARCHAR(500) +,repo_name VARCHAR(500) +,repo_full_name VARCHAR(1000) +,repo_avatar VARCHAR(500) +,repo_link VARCHAR(1000) +,repo_clone VARCHAR(1000) +,repo_branch VARCHAR(500) +,repo_timeout INTEGER +,repo_private BOOLEAN +,repo_trusted BOOLEAN +,repo_allow_pr BOOLEAN +,repo_allow_push BOOLEAN +,repo_allow_deploys BOOLEAN +,repo_allow_tags BOOLEAN +,repo_hash VARCHAR(500) + +,UNIQUE(repo_owner, repo_name) +); + +CREATE TABLE stars ( + star_id SERIAL PRIMARY KEY +,star_repo_id INTEGER +,star_user_id INTEGER + +,UNIQUE(star_repo_id, star_user_id) +); + +CREATE INDEX ix_star_user ON builds (star_user_id); + +CREATE TABLE keys ( + key_id SERIAL PRIMARY KEY +,key_repo_id INTEGER +,key_public BYTEA +,key_private BYTEA + +,UNIQUE(key_repo_id) +); + +CREATE TABLE builds ( + build_id SERIAL PRIMARY KEY +,build_repo_id INTEGER +,build_number INTEGER +,build_event VARCHAR(500) +,build_status VARCHAR(500) +,build_created INTEGER +,build_started INTEGER +,build_finished INTEGER +,build_commit VARCHAR(500) +,build_branch VARCHAR(500) +,build_ref VARCHAR(500) +,build_refspec VARCHAR(1000) +,build_remote VARCHAR(500) +,build_title VARCHAR(1000) +,build_message VARCHAR(2000) +,build_timestamp INTEGER +,build_author VARCHAR(500) +,build_avatar VARCHAR(1000) +,build_email VARCHAR(500) +,build_link VARCHAR(1000) + +,UNIQUE(build_number, build_repo_id) +); + +CREATE INDEX ix_build_repo ON builds (build_repo_id); + +CREATE TABLE jobs ( + job_id SERIAL PRIMARY KEY +,job_node_id INTEGER +,job_build_id INTEGER +,job_number INTEGER +,job_status VARCHAR(500) +,job_exit_code INTEGER +,job_started INTEGER +,job_finished INTEGER +,job_environment VARCHAR(2000) + +,UNIQUE(job_build_id, job_number) +); + +CREATE INDEX ix_job_build ON jobs (job_build_id); +CREATE INDEX ix_job_node ON jobs (job_node_id); + +CREATE TABLE IF NOT EXISTS logs ( + log_id SERIAL PRIMARY KEY +,log_job_id INTEGER +,log_data BYTEA + +,UNIQUE(log_job_id) +); + +CREATE TABLE IF NOT EXISTS nodes ( + node_id INTEGER PRIMARY KEY AUTOINCREMENT +,node_addr VARCHAR(1024) +,node_arch VARCHAR(50) +,node_cert BYTEA +,node_key BYTEA +,node_ca BYTEA +); + + +INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', ''); +INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', ''); + +-- +migrate Down + +DROP TABLE nodes; +DROP TABLE logs; +DROP TABLE jobs; +DROP TABLE builds; +DROP TABLE keys; +DROP TABLE stars; +DROP TABLE repos; +DROP TABLE users; \ No newline at end of file diff --git a/shared/database/rebind.go b/shared/database/rebind.go new file mode 100644 index 0000000000..4d2aa36647 --- /dev/null +++ b/shared/database/rebind.go @@ -0,0 +1,32 @@ +package database + +import ( + "strconv" + + "github.com/russross/meddler" +) + +// Rebind is a helper function that changes the sql +// bind type from ? to $ for postgres queries. +func Rebind(query string) string { + if meddler.Default != meddler.PostgreSQL { + return query + } + + qb := []byte(query) + // Add space enough for 5 params before we have to allocate + rqb := make([]byte, 0, len(qb)+5) + j := 1 + for _, b := range qb { + if b == '?' { + rqb = append(rqb, '$') + for _, b := range strconv.Itoa(j) { + rqb = append(rqb, byte(b)) + } + j++ + } else { + rqb = append(rqb, b) + } + } + return string(rqb) +} diff --git a/shared/database/sqlite3/1_init.sql b/shared/database/sqlite3/1_init.sql new file mode 100644 index 0000000000..511ff78e33 --- /dev/null +++ b/shared/database/sqlite3/1_init.sql @@ -0,0 +1,131 @@ +-- +migrate Up + +CREATE TABLE users ( + user_id INTEGER PRIMARY KEY AUTOINCREMENT +,user_login TEXT +,user_token TEXT +,user_secret TEXT +,user_email TEXT +,user_avatar TEXT +,user_active BOOLEAN +,user_admin BOOLEAN +,user_hash TEXT + +,UNIQUE(user_login) +); + +CREATE TABLE repos ( + repo_id INTEGER PRIMARY KEY AUTOINCREMENT +,repo_user_id INTEGER +,repo_owner TEXT +,repo_name TEXT +,repo_full_name TEXT +,repo_avatar TEXT +,repo_link TEXT +,repo_clone TEXT +,repo_branch TEXT +,repo_timeout INTEGER +,repo_private BOOLEAN +,repo_trusted BOOLEAN +,repo_allow_pr BOOLEAN +,repo_allow_push BOOLEAN +,repo_allow_deploys BOOLEAN +,repo_allow_tags BOOLEAN +,repo_hash TEXT + +,UNIQUE(repo_owner, repo_name) +); + +CREATE TABLE stars ( + star_id INTEGER PRIMARY KEY AUTOINCREMENT +,star_repo_id INTEGER +,star_user_id INTEGER + +,UNIQUE(star_repo_id, star_user_id) +); + +CREATE INDEX ix_star_user ON stars (star_user_id); + +CREATE TABLE keys ( + key_id INTEGER PRIMARY KEY AUTOINCREMENT +,key_repo_id INTEGER +,key_public BLOB +,key_private BLOB + +,UNIQUE(key_repo_id) +); + +CREATE TABLE builds ( + build_id INTEGER PRIMARY KEY AUTOINCREMENT +,build_repo_id INTEGER +,build_number INTEGER +,build_event TEXT +,build_status TEXT +,build_created INTEGER +,build_started INTEGER +,build_finished INTEGER +,build_commit TEXT +,build_branch TEXT +,build_ref TEXT +,build_refspec TEXT +,build_remote TEXT +,build_title TEXT +,build_message TEXT +,build_timestamp INTEGER +,build_author TEXT +,build_avatar TEXT +,build_email TEXT +,build_link TEXT + +,UNIQUE(build_number, build_repo_id) +); + +CREATE INDEX ix_build_repo ON builds (build_repo_id); + +CREATE TABLE jobs ( + job_id INTEGER PRIMARY KEY AUTOINCREMENT +,job_node_id INTEGER +,job_build_id INTEGER +,job_number INTEGER +,job_status TEXT +,job_exit_code INTEGER +,job_started INTEGER +,job_finished INTEGER +,job_environment TEXT + +,UNIQUE(job_build_id, job_number) +); + +CREATE INDEX ix_job_build ON jobs (job_build_id); +CREATE INDEX ix_job_node ON jobs (job_node_id); + +CREATE TABLE IF NOT EXISTS logs ( + log_id INTEGER PRIMARY KEY AUTOINCREMENT +,log_job_id INTEGER +,log_data BLOB + +,UNIQUE(log_job_id) +); + +CREATE TABLE IF NOT EXISTS nodes ( + node_id INTEGER PRIMARY KEY AUTOINCREMENT +,node_addr TEXT +,node_arch TEXT +,node_cert BLOB +,node_key BLOB +,node_ca BLOB +); + +INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', ''); +INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', ''); + +-- +migrate Down + +DROP TABLE nodes; +DROP TABLE logs; +DROP TABLE jobs; +DROP TABLE builds; +DROP TABLE keys; +DROP TABLE stars; +DROP TABLE repos; +DROP TABLE users; \ No newline at end of file diff --git a/shared/docker/docker.go b/shared/docker/docker.go new file mode 100644 index 0000000000..29ed10613d --- /dev/null +++ b/shared/docker/docker.go @@ -0,0 +1,109 @@ +package docker + +import ( + "io" + "io/ioutil" + + "github.com/samalba/dockerclient" +) + +var ( + LogOpts = &dockerclient.LogOptions{ + Stdout: true, + Stderr: true, + } + + LogOptsTail = &dockerclient.LogOptions{ + Follow: true, + Stdout: true, + Stderr: true, + } +) + +// Run creates the docker container, pulling images if necessary, starts +// the container and blocks until the container exits, returning the exit +// information. +func Run(client dockerclient.Client, conf *dockerclient.ContainerConfig, name string) (*dockerclient.ContainerInfo, error) { + info, err := RunDaemon(client, conf, name) + if err != nil { + return nil, err + } + + return Wait(client, info.Id) +} + +// RunDaemon creates the docker container, pulling images if necessary, starts +// the container and returns the container information. It does not wait for +// the container to exit. +func RunDaemon(client dockerclient.Client, conf *dockerclient.ContainerConfig, name string) (*dockerclient.ContainerInfo, error) { + + // attempts to create the contianer + id, err := client.CreateContainer(conf, name) + if err != nil { + // and pull the image and re-create if that fails + err = client.PullImage(conf.Image, nil) + if err != nil { + return nil, err + } + id, err = client.CreateContainer(conf, name) + if err != nil { + client.RemoveContainer(id, true, true) + return nil, err + } + } + + // fetches the container information + info, err := client.InspectContainer(id) + if err != nil { + client.RemoveContainer(id, true, true) + return nil, err + } + + // starts the container + err = client.StartContainer(id, &conf.HostConfig) + if err != nil { + client.RemoveContainer(id, true, true) + return nil, err + } + + return info, err +} + +// Wait blocks until the named container exits, returning the exit information. +func Wait(client dockerclient.Client, name string) (*dockerclient.ContainerInfo, error) { + + defer func() { + client.StopContainer(name, 5) + client.KillContainer(name, "9") + }() + + errc := make(chan error, 1) + infoc := make(chan *dockerclient.ContainerInfo, 1) + go func() { + + // blocks and waits for the container to finish + // by streaming the logs (to /dev/null). Ideally + // we could use the `wait` function instead + rc, err := client.ContainerLogs(name, LogOptsTail) + if err != nil { + errc <- err + return + } + io.Copy(ioutil.Discard, rc) + rc.Close() + + info, err := client.InspectContainer(name) + if err != nil { + errc <- err + return + } + infoc <- info + }() + + select { + case info := <-infoc: + return info, nil + case err := <-errc: + return nil, err + } +} diff --git a/shared/envconfig/envconfig.go b/shared/envconfig/envconfig.go new file mode 100644 index 0000000000..26059aacbc --- /dev/null +++ b/shared/envconfig/envconfig.go @@ -0,0 +1,117 @@ +package envconfig + +import ( + "bufio" + "errors" + "os" + "strconv" + "strings" +) + +type Env map[string]string + +// Get returns the value of the environment variable named by the key. +func (env Env) Get(key string) string { + return env[key] +} + +// String returns the string value of the environment variable named by the +// key. If the variable is not present, the default value is returned. +func (env Env) String(key, value string) string { + got, ok := env[key] + if ok { + value = got + } + return value +} + +// Bool returns the boolean value of the environment variable named by the key. +// If the variable is not present, the default value is returned. +func (env Env) Bool(name string, value bool) bool { + got, ok := env[name] + if ok { + value, _ = strconv.ParseBool(got) + } + return value +} + +// Int returns the integer value of the environment variable named by the key. +// If the variable is not present, the default value is returned. +func (env Env) Int(name string, value int) int { + got, ok := env[name] + if ok { + value, _ = strconv.Atoi(got) + } + return value +} + +// Load reads the environment file and reads variables in "key=value" format. +// Then it read the system environment variables. It returns the combined +// results in a key value map. +func Load(filepath string) Env { + var envs = map[string]string{} + + // load the environment file + f, err := os.Open(filepath) + if err == nil { + defer f.Close() + + r := bufio.NewReader(f) + for { + line, _, err := r.ReadLine() + if err != nil { + break + } + + key, val, err := parseln(string(line)) + if err != nil { + continue + } + + os.Setenv(key, val) + } + } + + // load the environment variables + for _, env := range os.Environ() { + key, val, err := parseln(env) + if err != nil { + continue + } + + envs[key] = val + } + + return Env(envs) +} + +// helper function to parse a "key=value" environment variable string. +func parseln(line string) (key string, val string, err error) { + line = removeComments(line) + if len(line) == 0 { + return + } + splits := strings.SplitN(line, "=", 2) + + if len(splits) < 2 { + err = errors.New("missing delimiter '='") + return + } + + key = strings.Trim(splits[0], " ") + val = strings.Trim(splits[1], ` "'`) + return +} + +// helper function to trim comments and whitespace from a string. +func removeComments(s string) (_ string) { + if len(s) == 0 || string(s[0]) == "#" { + return + } else { + index := strings.Index(s, " #") + if index > -1 { + s = strings.TrimSpace(s[0:index]) + } + } + return s +} diff --git a/shared/httputil/httputil.go b/shared/httputil/httputil.go index c22c16933f..86cc7b5a11 100644 --- a/shared/httputil/httputil.go +++ b/shared/httputil/httputil.go @@ -92,6 +92,7 @@ func SetCookie(w http.ResponseWriter, r *http.Request, name, value string) { Domain: r.URL.Host, HttpOnly: true, Secure: IsHttps(r), + MaxAge: 2147483647, // the cooke value (token) is responsible for expiration } http.SetCookie(w, &cookie) diff --git a/shared/oauth2/oauth2.go b/shared/oauth2/oauth2.go new file mode 100644 index 0000000000..97059c4993 --- /dev/null +++ b/shared/oauth2/oauth2.go @@ -0,0 +1,471 @@ +// Copyright 2011 The goauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package oauth supports making OAuth2-authenticated HTTP requests. +// +// Example usage: +// +// // Specify your configuration. (typically as a global variable) +// var config = &oauth.Config{ +// ClientId: YOUR_CLIENT_ID, +// ClientSecret: YOUR_CLIENT_SECRET, +// Scope: "https://www.googleapis.com/auth/buzz", +// AuthURL: "https://accounts.google.com/o/oauth2/auth", +// TokenURL: "https://accounts.google.com/o/oauth2/token", +// RedirectURL: "http://you.example.org/handler", +// } +// +// // A landing page redirects to the OAuth provider to get the auth code. +// func landing(w http.ResponseWriter, r *http.Request) { +// http.Redirect(w, r, config.AuthCodeURL("foo"), http.StatusFound) +// } +// +// // The user will be redirected back to this handler, that takes the +// // "code" query parameter and Exchanges it for an access token. +// func handler(w http.ResponseWriter, r *http.Request) { +// t := &oauth.Transport{Config: config} +// t.Exchange(r.FormValue("code")) +// // The Transport now has a valid Token. Create an *http.Client +// // with which we can make authenticated API requests. +// c := t.Client() +// c.Post(...) +// // ... +// // btw, r.FormValue("state") == "foo" +// } +// +package oauth2 + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "sync" + "time" +) + +// OAuthError is the error type returned by many operations. +// +// In retrospect it should not exist. Don't depend on it. +type OAuthError struct { + prefix string + msg string +} + +func (oe OAuthError) Error() string { + return "OAuthError: " + oe.prefix + ": " + oe.msg +} + +// Cache specifies the methods that implement a Token cache. +type Cache interface { + Token() (*Token, error) + PutToken(*Token) error +} + +// CacheFile implements Cache. Its value is the name of the file in which +// the Token is stored in JSON format. +type CacheFile string + +func (f CacheFile) Token() (*Token, error) { + file, err := os.Open(string(f)) + if err != nil { + return nil, OAuthError{"CacheFile.Token", err.Error()} + } + defer file.Close() + tok := &Token{} + if err := json.NewDecoder(file).Decode(tok); err != nil { + return nil, OAuthError{"CacheFile.Token", err.Error()} + } + return tok, nil +} + +func (f CacheFile) PutToken(tok *Token) error { + file, err := os.OpenFile(string(f), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return OAuthError{"CacheFile.PutToken", err.Error()} + } + if err := json.NewEncoder(file).Encode(tok); err != nil { + file.Close() + return OAuthError{"CacheFile.PutToken", err.Error()} + } + if err := file.Close(); err != nil { + return OAuthError{"CacheFile.PutToken", err.Error()} + } + return nil +} + +// Config is the configuration of an OAuth consumer. +type Config struct { + // ClientId is the OAuth client identifier used when communicating with + // the configured OAuth provider. + ClientId string + + // ClientSecret is the OAuth client secret used when communicating with + // the configured OAuth provider. + ClientSecret string + + // Scope identifies the level of access being requested. Multiple scope + // values should be provided as a space-delimited string. + Scope string + + // AuthURL is the URL the user will be directed to in order to grant + // access. + AuthURL string + + // TokenURL is the URL used to retrieve OAuth tokens. + TokenURL string + + // RedirectURL is the URL to which the user will be returned after + // granting (or denying) access. + RedirectURL string + + // TokenCache allows tokens to be cached for subsequent requests. + TokenCache Cache + + // AccessType is an OAuth extension that gets sent as the + // "access_type" field in the URL from AuthCodeURL. + // See https://developers.google.com/accounts/docs/OAuth2WebServer. + // It may be "online" (the default) or "offline". + // If your application needs to refresh access tokens when the + // user is not present at the browser, then use offline. This + // will result in your application obtaining a refresh token + // the first time your application exchanges an authorization + // code for a user. + AccessType string + + // ApprovalPrompt indicates whether the user should be + // re-prompted for consent. If set to "auto" (default) the + // user will be prompted only if they haven't previously + // granted consent and the code can only be exchanged for an + // access token. + // If set to "force" the user will always be prompted, and the + // code can be exchanged for a refresh token. + ApprovalPrompt string +} + +// Token contains an end-user's tokens. +// This is the data you must store to persist authentication. +type Token struct { + AccessToken string + RefreshToken string + Expiry time.Time // If zero the token has no (known) expiry time. + + // Extra optionally contains extra metadata from the server + // when updating a token. The only current key that may be + // populated is "id_token". It may be nil and will be + // initialized as needed. + Extra map[string]string +} + +// Expired reports whether the token has expired or is invalid. +func (t *Token) Expired() bool { + if t.AccessToken == "" { + return true + } + if t.Expiry.IsZero() { + return false + } + return t.Expiry.Before(time.Now()) +} + +// Transport implements http.RoundTripper. When configured with a valid +// Config and Token it can be used to make authenticated HTTP requests. +// +// t := &oauth.Transport{config} +// t.Exchange(code) +// // t now contains a valid Token +// r, _, err := t.Client().Get("http://example.org/url/requiring/auth") +// +// It will automatically refresh the Token if it can, +// updating the supplied Token in place. +type Transport struct { + *Config + *Token + + // mu guards modifying the token. + mu sync.Mutex + + // Transport is the HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + // (It should never be an oauth.Transport.) + Transport http.RoundTripper +} + +// Client returns an *http.Client that makes OAuth-authenticated requests. +func (t *Transport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *Transport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + +// AuthCodeURL returns a URL that the end-user should be redirected to, +// so that they may obtain an authorization code. +func (c *Config) AuthCodeURL(state string) string { + url_, err := url.Parse(c.AuthURL) + if err != nil { + panic("AuthURL malformed: " + err.Error()) + } + q := url.Values{ + "response_type": {"code"}, + "client_id": {c.ClientId}, + "state": condVal(state), + "scope": condVal(c.Scope), + "redirect_uri": condVal(c.RedirectURL), + "access_type": condVal(c.AccessType), + "approval_prompt": condVal(c.ApprovalPrompt), + }.Encode() + if url_.RawQuery == "" { + url_.RawQuery = q + } else { + url_.RawQuery += "&" + q + } + return url_.String() +} + +func condVal(v string) []string { + if v == "" { + return nil + } + return []string{v} +} + +// Exchange takes a code and gets access Token from the remote server. +func (t *Transport) Exchange(code string) (*Token, error) { + if t.Config == nil { + return nil, OAuthError{"Exchange", "no Config supplied"} + } + + // If the transport or the cache already has a token, it is + // passed to `updateToken` to preserve existing refresh token. + tok := t.Token + if tok == nil && t.TokenCache != nil { + tok, _ = t.TokenCache.Token() + } + if tok == nil { + tok = new(Token) + } + err := t.updateToken(tok, url.Values{ + "grant_type": {"authorization_code"}, + "redirect_uri": {t.RedirectURL}, + "scope": {t.Scope}, + "code": {code}, + }) + if err != nil { + return nil, err + } + t.Token = tok + if t.TokenCache != nil { + return tok, t.TokenCache.PutToken(tok) + } + return tok, nil +} + +// RoundTrip executes a single HTTP transaction using the Transport's +// Token as authorization headers. +// +// This method will attempt to renew the Token if it has expired and may return +// an error related to that Token renewal before attempting the client request. +// If the Token cannot be renewed a non-nil os.Error value will be returned. +// If the Token is invalid callers should expect HTTP-level errors, +// as indicated by the Response's StatusCode. +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + accessToken, err := t.getAccessToken() + if err != nil { + return nil, err + } + // To set the Authorization header, we must make a copy of the Request + // so that we don't modify the Request we were given. + // This is required by the specification of http.RoundTripper. + req = cloneRequest(req) + req.Header.Set("Authorization", "Bearer "+accessToken) + + // Make the HTTP request. + return t.transport().RoundTrip(req) +} + +func (t *Transport) getAccessToken() (string, error) { + t.mu.Lock() + defer t.mu.Unlock() + + if t.Token == nil { + if t.Config == nil { + return "", OAuthError{"RoundTrip", "no Config supplied"} + } + if t.TokenCache == nil { + return "", OAuthError{"RoundTrip", "no Token supplied"} + } + var err error + t.Token, err = t.TokenCache.Token() + if err != nil { + return "", err + } + } + + // Refresh the Token if it has expired. + if t.Expired() { + if err := t.Refresh(); err != nil { + return "", err + } + } + if t.AccessToken == "" { + return "", errors.New("no access token obtained from refresh") + } + return t.AccessToken, nil +} + +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header) + for k, s := range r.Header { + r2.Header[k] = s + } + return r2 +} + +// Refresh renews the Transport's AccessToken using its RefreshToken. +func (t *Transport) Refresh() error { + if t.Token == nil { + return OAuthError{"Refresh", "no existing Token"} + } + if t.RefreshToken == "" { + return OAuthError{"Refresh", "Token expired; no Refresh Token"} + } + if t.Config == nil { + return OAuthError{"Refresh", "no Config supplied"} + } + + err := t.updateToken(t.Token, url.Values{ + "grant_type": {"refresh_token"}, + "refresh_token": {t.RefreshToken}, + }) + if err != nil { + return err + } + if t.TokenCache != nil { + return t.TokenCache.PutToken(t.Token) + } + return nil +} + +// AuthenticateClient gets an access Token using the client_credentials grant +// type. +func (t *Transport) AuthenticateClient() error { + if t.Config == nil { + return OAuthError{"Exchange", "no Config supplied"} + } + if t.Token == nil { + t.Token = &Token{} + } + return t.updateToken(t.Token, url.Values{"grant_type": {"client_credentials"}}) +} + +// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL +// implements the OAuth2 spec correctly +// See https://code.google.com/p/goauth2/issues/detail?id=31 for background. +// In summary: +// - Reddit only accepts client secret in the Authorization header +// - Dropbox accepts either it in URL param or Auth header, but not both. +// - Google only accepts URL param (not spec compliant?), not Auth header +func providerAuthHeaderWorks(tokenURL string) bool { + if strings.HasPrefix(tokenURL, "https://accounts.google.com/") || + strings.HasPrefix(tokenURL, "https://github.com/") || + strings.HasPrefix(tokenURL, "https://api.instagram.com/") || + strings.HasPrefix(tokenURL, "https://www.douban.com/") { + // Some sites fail to implement the OAuth2 spec fully. + return false + } + + // Assume the provider implements the spec properly + // otherwise. We can add more exceptions as they're + // discovered. We will _not_ be adding configurable hooks + // to this package to let users select server bugs. + return true +} + +// updateToken mutates both tok and v. +func (t *Transport) updateToken(tok *Token, v url.Values) error { + v.Set("client_id", t.ClientId) + v.Set("client_secret", t.ClientSecret) + client := &http.Client{Transport: t.transport()} + req, err := http.NewRequest("POST", t.TokenURL, strings.NewReader(v.Encode())) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(t.ClientId, t.ClientSecret) + r, err := client.Do(req) + if err != nil { + return err + } + defer r.Body.Close() + if r.StatusCode != 200 { + return OAuthError{"updateToken", "Unexpected HTTP status " + r.Status} + } + var b struct { + Access string `json:"access_token"` + Refresh string `json:"refresh_token"` + ExpiresIn int64 `json:"expires_in"` // seconds + Id string `json:"id_token"` + } + + body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + return err + } + + content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + switch content { + case "application/x-www-form-urlencoded", "text/plain": + vals, err := url.ParseQuery(string(body)) + if err != nil { + return err + } + + b.Access = vals.Get("access_token") + b.Refresh = vals.Get("refresh_token") + b.ExpiresIn, _ = strconv.ParseInt(vals.Get("expires_in"), 10, 64) + b.Id = vals.Get("id_token") + default: + if err = json.Unmarshal(body, &b); err != nil { + return fmt.Errorf("got bad response from server: %q", body) + } + } + if b.Access == "" { + return errors.New("received empty access token from authorization server") + } + tok.AccessToken = b.Access + // Don't overwrite `RefreshToken` with an empty value + if b.Refresh != "" { + tok.RefreshToken = b.Refresh + } + if b.ExpiresIn == 0 { + tok.Expiry = time.Time{} + } else { + tok.Expiry = time.Now().Add(time.Duration(b.ExpiresIn) * time.Second) + } + if b.Id != "" { + if tok.Extra == nil { + tok.Extra = make(map[string]string) + } + tok.Extra["id_token"] = b.Id + } + return nil +} diff --git a/shared/server/server.go b/shared/server/server.go new file mode 100644 index 0000000000..9c3fafe8af --- /dev/null +++ b/shared/server/server.go @@ -0,0 +1,36 @@ +package server + +import ( + "net/http" + + log "github.com/Sirupsen/logrus" + "github.com/drone/drone/shared/envconfig" +) + +type Server struct { + Addr string + Cert string + Key string +} + +func Load(env envconfig.Env) *Server { + return &Server{ + Addr: env.String("SERVER_ADDR", ":8000"), + Cert: env.String("SERVER_CERT", ""), + Key: env.String("SERVER_KEY", ""), + } +} + +func (s *Server) Run(handler http.Handler) { + log.Infof("starting server %s", s.Addr) + + if len(s.Cert) != 0 { + log.Fatal( + http.ListenAndServeTLS(s.Addr, s.Cert, s.Key, handler), + ) + } else { + log.Fatal( + http.ListenAndServe(s.Addr, handler), + ) + } +} diff --git a/shared/token/token.go b/shared/token/token.go index b371c97831..78ac04f75f 100644 --- a/shared/token/token.go +++ b/shared/token/token.go @@ -1,9 +1,10 @@ package token import ( + "fmt" "net/http" - "github.com/drone/drone/Godeps/_workspace/src/github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" ) type SecretFunc func(*Token) (string, error) @@ -12,6 +13,7 @@ const ( UserToken = "user" SessToken = "sess" HookToken = "hook" + CsrfToken = "csrf" ) // Default algorithm used to sign JWT tokens. @@ -22,7 +24,6 @@ type Token struct { Text string } -// Parse parses func Parse(raw string, fn SecretFunc) (*Token, error) { token := &Token{} parsed, err := jwt.Parse(raw, keyFunc(token, fn)) @@ -34,15 +35,46 @@ func Parse(raw string, fn SecretFunc) (*Token, error) { return token, nil } -func ParseRequest(req *http.Request, fn SecretFunc) (*Token, error) { - token := &Token{} - parsed, err := jwt.ParseFromRequest(req, keyFunc(token, fn)) +func ParseRequest(r *http.Request, fn SecretFunc) (*Token, error) { + var token = r.Header.Get("Authorization") + + // first we attempt to get the token from the + // authorization header. + if len(token) != 0 { + token = r.Header.Get("Authorization") + fmt.Sscanf(token, "Bearer %s", &token) + return Parse(token, fn) + } + + // then we attempt to get the token from the + // access_token url query parameter + token = r.FormValue("access_token") + if len(token) != 0 { + return Parse(token, fn) + } + + // and finally we attemt to get the token from + // the user session cookie + cookie, err := r.Cookie("user_sess") if err != nil { return nil, err - } else if !parsed.Valid { - return nil, jwt.ValidationError{} } - return token, nil + return Parse(cookie.Value, fn) +} + +func CheckCsrf(r *http.Request, fn SecretFunc) error { + + // get and options requests are always + // enabled, without CSRF checks. + switch r.Method { + case "GET", "OPTIONS": + return nil + } + + // parse the raw CSRF token value and validate + raw := r.Header.Get("X-CSRF-TOKEN") + _, err := Parse(raw, fn) + return err } func New(kind, text string) *Token { diff --git a/shared/token/token_test.go b/shared/token/token_test.go deleted file mode 100644 index 1765cc067f..0000000000 --- a/shared/token/token_test.go +++ /dev/null @@ -1 +0,0 @@ -package token diff --git a/static/images/docker.svg b/static/images/docker.svg new file mode 100644 index 0000000000..ae937ca1ca --- /dev/null +++ b/static/images/docker.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/images/docker_blue.svg b/static/images/docker_blue.svg new file mode 100644 index 0000000000..bb007f66e2 --- /dev/null +++ b/static/images/docker_blue.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/images/docker_white.svg b/static/images/docker_white.svg new file mode 100644 index 0000000000..519cd20814 --- /dev/null +++ b/static/images/docker_white.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/images/favicon.ico b/static/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a7607313a63dd76fb880228ce6ed6fa75b4678b1 GIT binary patch literal 1150 zcmb`HJxjw-6oyYf)Pjfwr3E|Kv<^ZSrGnzrK^z>^NeV^OP4EwBh0>y6adP(`IJk(T zC=T`y2vQeC2kBN(K@pA5ZFBJ&Vo|i^$(wV|d(KU6LPUK0!(rhVm*t>HL_`LG6q#lo z5utX`)ua(~uxeCc`n)i-JC=sF@@*V z=zHh<&4_J0J6`?CWO4xCljD6sSI9-U_Aos;;s>W@tNU@!pth^)_?PgE5+6f_c!GFF z*U>YxU)>X)5|pWJWHOl`dTRKq%Fm2~@}OJMXtY!1@MK}pAm@0`js81l=A7YEJ@as6 z`1GEueVm3{sIlGxIu5$7wL)&)BVQvo#(f3S>2w?CLvZ8aQ}4%!M_$iY>tD5~4?BCX z-NK`1xwWi%oqj+4hrgI{fcMLLNF0JZJc4^I9p6)1gOFa^t(qSg3Z@CZMDMW2X5dFtKj+adA+p{&De literal 0 HcmV?d00001 diff --git a/static/images/favicon.png b/static/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c91951dc265b84151105ee56220ef9ecf3ea8057 GIT binary patch literal 557 zcmV+|0@D47P)agldo@C`vUU?E0$^&XeJ_$V)6@xq16?|kPE z_naeU#{ZnkULk~V7`OqP0FF=OE#L$2OjW-XdC4mXAyk2N;1;kjBjFCv0UoOAuW5h~ zf&*Rw?SHKNHE>B)qnikweK-TG0`~#Ld%py{p8=-8F0eSme#QakOAKFuv%Ow#u~w@c z1x^EBO1O&-SS~SK1rB=eKM#k))XYu+YbD%@c<_!ySv`|W6rtx7^98H<9ma_U{sugr@(ap z5xEv){3s$FpeiDFVvGwSvN;L(PF2qV05rXZj}Otq`u00000NkvXXu0mjf(v9k6 literal 0 HcmV?d00001 diff --git a/static/images/logo_dark.svg b/static/images/logo_dark.svg new file mode 100644 index 0000000000..be92f1150d --- /dev/null +++ b/static/images/logo_dark.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/static/images/logo_light.svg b/static/images/logo_light.svg new file mode 100644 index 0000000000..af16f28739 --- /dev/null +++ b/static/images/logo_light.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/static/images/ubuntu.svg b/static/images/ubuntu.svg new file mode 100644 index 0000000000..850665f7a1 --- /dev/null +++ b/static/images/ubuntu.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/static/scripts/build.js b/static/scripts/build.js new file mode 100644 index 0000000000..609d077ca5 --- /dev/null +++ b/static/scripts/build.js @@ -0,0 +1,120 @@ + + +function JobViewModel(repo, build, job, status) { + var self = this; + self.status = status; + + + if (status !== "running" && status !== "pending") { + Logs(repo, build, job); + } + + if (status === "running") { + Stream(repo, build, job, function(out){ + $( "#output" ).append(out); + }); + } + + $("#restart").click(function() { + $("#restart").hide(); + $("#output").html(""); + $(".status").attr("class", "status pending").text("pending"); + + $.ajax({ + url: "/api/repos/"+repo+"/builds/"+build, + type: "POST", + success: function( data ) { }, + error: function( data ) { + console.log(data); + } + }); + }) + + + Subscribe(repo, function(data){ + if (!data.jobs) { + return; + } + + var before = self.status; + self.status = data.jobs[job-1].status; + + // update the status for each job in the view + for (var i=0;i b.full_name().toLowerCase() ? 1 : -1; +} + +/** + * Creates an observable object that stores a list of hook event + * types (push, pull request, etc) and true or false if enabled. + */ +function Hook(repo) { + var data = { + "pull_request" : repo.events.indexOf("pull_request") !== -1, + "push" : repo.events.indexOf("push") !== -1, + "tag" : repo.events.indexOf("tag") !== -1, + "deploy" : repo.events.indexOf("deploy") !== -1 + }; + + this.pull_request = ko.observable(data.pull_request); + this.push = ko.observable(data.push); + this.tag = ko.observable(data.tag); + this.deploy = ko.observable(data.deploy); +} + +/** + * Creates an observable user. + */ +function User(data) { + this.login = ko.observable(data.login); + this.email = ko.observable(data.email); + this.avatar_url = ko.observable(data.avatar_url); + this.active = ko.observable(data.active); + this.admin = ko.observable(data.admin); +} + +/** + * Compares two user objects by login. Used to sort + * a list of users. + */ +function UserCompare(a, b) { + return a.login().toLowerCase() > b.login().toLowerCase() ? 1 : -1; +} diff --git a/static/scripts/nodes.js b/static/scripts/nodes.js new file mode 100644 index 0000000000..cc059ea751 --- /dev/null +++ b/static/scripts/nodes.js @@ -0,0 +1,69 @@ + +function NodeViewModel() { + var self = this; + + // handle requests to create a new node. + $(".modal-node button").click(function(e) { + var addr = $(".modal-node input").val(); + var node = { address: addr }; + + $.ajax({ + url: "/api/nodes", + type: "POST", + contentType: "application/json", + data: JSON.stringify(node), + success: function( data ) { + // clears the form value + $(".modal-node input").val(""); + + var el = $("
").attr("class", "col-sm-4").append( + $("
").attr("class", "card").attr("data-id", data.id).append( + $("
").attr("class", "card-header").append( + $("").attr("class", "linux_amd64") + ) + ).append( + $("
").attr("class", "card-block").append( + $("

").text(data.address) + ).append( + $("

").attr("class", "card-text").text(data.architecture) + ).append( + $("

").attr("class", "btn-group").append( + $("