mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-02-17 03:45:13 +00:00
Merge remote-tracking branch 'drone/master' into fail-pending-on-startup
Conflicts: cmd/droned/drone.go
This commit is contained in:
commit
efb5d4e0cf
45 changed files with 1827 additions and 358 deletions
43
Makefile
43
Makefile
|
@ -1,5 +1,24 @@
|
|||
SHA := $(shell git rev-parse --short HEAD)
|
||||
BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
|
||||
PKGS := \
|
||||
build \
|
||||
build/buildfile \
|
||||
build/docker \
|
||||
build/dockerfile \
|
||||
build/proxy \
|
||||
build/repo \
|
||||
build/script \
|
||||
channel \
|
||||
database \
|
||||
database/encrypt \
|
||||
database/migrate \
|
||||
database/testing \
|
||||
mail \
|
||||
model \
|
||||
plugin/deploy \
|
||||
queue
|
||||
PKGS := $(addprefix github.com/drone/drone/pkg/,$(PKGS))
|
||||
.PHONY := test $(PKGS)
|
||||
|
||||
all: embed build
|
||||
|
||||
|
@ -25,6 +44,7 @@ deps:
|
|||
go get github.com/drone/go-bitbucket/bitbucket
|
||||
go get github.com/GeertJohan/go.rice
|
||||
go get github.com/GeertJohan/go.rice/rice
|
||||
go get github.com/go-sql-driver/mysql
|
||||
go get github.com/mattn/go-sqlite3
|
||||
go get github.com/russross/meddler
|
||||
|
||||
|
@ -36,26 +56,13 @@ js:
|
|||
cd cmd/droned/assets && find js -name "*.js" ! -name '.*' ! -name "main.js" -exec cat {} \; > js/main.js
|
||||
|
||||
build:
|
||||
cd cmd/drone && go build -o ../../bin/drone
|
||||
cd cmd/drone && go build -ldflags "-X main.version $(SHA)" -o ../../bin/drone
|
||||
cd cmd/droned && go build -ldflags "-X main.version $(SHA)" -o ../../bin/droned
|
||||
|
||||
test:
|
||||
go test -v github.com/drone/drone/pkg/build
|
||||
go test -v github.com/drone/drone/pkg/build/buildfile
|
||||
go test -v github.com/drone/drone/pkg/build/docker
|
||||
go test -v github.com/drone/drone/pkg/build/dockerfile
|
||||
go test -v github.com/drone/drone/pkg/build/proxy
|
||||
go test -v github.com/drone/drone/pkg/build/repo
|
||||
go test -v github.com/drone/drone/pkg/build/script
|
||||
go test -v github.com/drone/drone/pkg/channel
|
||||
go test -v github.com/drone/drone/pkg/database
|
||||
go test -v github.com/drone/drone/pkg/database/encrypt
|
||||
go test -v github.com/drone/drone/pkg/database/migrate
|
||||
go test -v github.com/drone/drone/pkg/database/testing
|
||||
go test -v github.com/drone/drone/pkg/mail
|
||||
go test -v github.com/drone/drone/pkg/model
|
||||
go test -v github.com/drone/drone/pkg/plugin/deploy
|
||||
go test -v github.com/drone/drone/pkg/queue
|
||||
test: $(PKGS)
|
||||
|
||||
$(PKGS):
|
||||
go test -v $@
|
||||
|
||||
install:
|
||||
cp deb/drone/etc/init/drone.conf /etc/init/drone.conf
|
||||
|
|
52
README.md
52
README.md
|
@ -64,6 +64,27 @@ you can still get a feel for the steps:
|
|||
|
||||
https://docs.google.com/file/d/0By8deR1ROz8memUxV0lTSGZPQUk
|
||||
|
||||
**Using MySQL**
|
||||
|
||||
By default, Drone use sqlite as its database storage. To use MySQL/MariaDB instead, use `-driver` flag
|
||||
and set it to `mysql`. You will need to set your DSN (`-datasource`) in this form:
|
||||
```
|
||||
user:password@tcp(hostname:port)/dbname?parseTime=true
|
||||
```
|
||||
Change it according to your database settings. The parseTime above is required since drone using
|
||||
`time.Time` to represents `TIMESTAMP` data. Please refer to [1] for more options on mysql driver.
|
||||
|
||||
You may also need to tweak some innodb options, especially if you're using `utf8mb4` collation type.
|
||||
```
|
||||
innodb_file_format = Barracuda
|
||||
innodb_file_per_table = On
|
||||
innodb_large_prefix = On
|
||||
```
|
||||
Please consult to the MySQL/MariaDB documentation for further information
|
||||
regarding large prefix for index column and dynamic row format (which is used in Drone).
|
||||
|
||||
[1] https://github.com/go-sql-driver/mysql
|
||||
|
||||
### Builds
|
||||
|
||||
Drone use a **.drone.yml** configuration file in the root of your
|
||||
|
@ -199,6 +220,15 @@ publish:
|
|||
source: /tmp/drone.deb
|
||||
target: latest/
|
||||
|
||||
swift:
|
||||
username: someuser
|
||||
password: 030e39a1278a18828389b194b93211aa
|
||||
auth_url: https://identity.api.rackspacecloud.com/v2.0
|
||||
region: DFW
|
||||
container: drone
|
||||
source: /tmp/drone.deb
|
||||
target: latest/drone.deb
|
||||
|
||||
```
|
||||
|
||||
Drone currently has these `deploy` and `publish` plugins implemented (more to come!):
|
||||
|
@ -209,9 +239,12 @@ Drone currently has these `deploy` and `publish` plugins implemented (more to co
|
|||
- [modulus](#docs)
|
||||
- [nodejitsu](#docs)
|
||||
- [ssh](#docs)
|
||||
- [tsuru](#docs)
|
||||
- [bash](#docs)
|
||||
|
||||
**publish**
|
||||
- [Amazon s3](#docs)
|
||||
- [OpenStack Swift](#docs)
|
||||
|
||||
### Notifications
|
||||
|
||||
|
@ -268,11 +301,22 @@ services:
|
|||
|
||||
If you omit the version, Drone will launch the latest version of the database. (For example, if you set `mongodb`, Drone will launch MongoDB 2.4.)
|
||||
|
||||
**NOTE:** database and service containers are exposed over TCP connections and
|
||||
You can also launch custom Docker containers using standard docker notation:
|
||||
|
||||
```sh
|
||||
services:
|
||||
- dockerfile/rethinkdb # same as dockerfile/rethinkdb:latest
|
||||
- barnybug/elasticsearch:1.0.1
|
||||
```
|
||||
|
||||
**NOTE 1:** database and service containers are exposed over TCP connections and
|
||||
have their own local IP address. If the **socat** utility is installed inside your
|
||||
Docker image, Drone will automatically proxy localhost connections to the correct
|
||||
IP address.
|
||||
|
||||
**NOTE 2:** avoid running services that use the same ports. For example, don't specify
|
||||
multiple versions of Elastic Search since the port will already be in use.
|
||||
|
||||
### Caching
|
||||
|
||||
Drone can persist directories between builds. This should be used for caching dependencies to
|
||||
|
@ -315,7 +359,11 @@ Local Drone setup for development is pretty straightforward.
|
|||
You will need to clone the repo, install Vagrant and run `vagrant up`.
|
||||
This command will download base Ubuntu image, setup the virtual machine and build Drone.
|
||||
|
||||
Afterwards, you will need to [install Docker in this VM manually](http://docs.docker.io/en/latest/installation/ubuntulinux/).
|
||||
Afterwards, you may `vagrant ssh` into the vagrant instance, where docker is already installed and ready to go.
|
||||
|
||||
Once in the vagrant instance, run `make run`, the visit http://localhost:8080/install in your browser.
|
||||
|
||||
The Makefile has other targets so check that out for more build, test, run configurations.
|
||||
|
||||
### Docs
|
||||
|
||||
|
|
|
@ -39,6 +39,10 @@ var (
|
|||
|
||||
// displays the help / usage if True
|
||||
help = flag.Bool("h", false, "")
|
||||
|
||||
// version number, currently deterined by the
|
||||
// git revision number (sha)
|
||||
version string
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -103,6 +107,10 @@ func main() {
|
|||
path = filepath.Join(path, ".drone.yml")
|
||||
vet(path)
|
||||
|
||||
// print the version / revision number
|
||||
case args[0] == "version" && len(args) == 1:
|
||||
println(version)
|
||||
|
||||
// print the help message
|
||||
case args[0] == "help" && len(args) == 1:
|
||||
flag.Usage()
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
|
@ -12,23 +11,15 @@ import (
|
|||
"code.google.com/p/go.net/websocket"
|
||||
"github.com/GeertJohan/go.rice"
|
||||
"github.com/bmizerany/pat"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/russross/meddler"
|
||||
|
||||
"github.com/drone/drone/pkg/build/docker"
|
||||
"github.com/drone/drone/pkg/channel"
|
||||
"github.com/drone/drone/pkg/database"
|
||||
"github.com/drone/drone/pkg/database/migrate"
|
||||
"github.com/drone/drone/pkg/handler"
|
||||
"github.com/drone/drone/pkg/queue"
|
||||
)
|
||||
|
||||
var (
|
||||
// local path where the SQLite database
|
||||
// should be stored. By default this is
|
||||
// in the current working directory.
|
||||
path string
|
||||
|
||||
// port the server will run on
|
||||
port string
|
||||
|
||||
|
@ -57,7 +48,6 @@ var (
|
|||
|
||||
func main() {
|
||||
// parse command line flags
|
||||
flag.StringVar(&path, "path", "", "")
|
||||
flag.StringVar(&port, "port", ":8080", "")
|
||||
flag.StringVar(&driver, "driver", "sqlite3", "")
|
||||
flag.StringVar(&datasource, "datasource", "drone.sqlite", "")
|
||||
|
@ -71,7 +61,9 @@ func main() {
|
|||
checkTLSFlags()
|
||||
|
||||
// setup database and handlers
|
||||
setupDatabase()
|
||||
if err := database.Init(driver, datasource); err != nil {
|
||||
log.Fatal("Can't initialize database: ", err)
|
||||
}
|
||||
discardOldBuilds()
|
||||
setupStatic()
|
||||
setupHandlers()
|
||||
|
@ -98,25 +90,6 @@ func checkTLSFlags() {
|
|||
|
||||
}
|
||||
|
||||
// setup the database connection and register with the
|
||||
// global database package.
|
||||
func setupDatabase() {
|
||||
// inform meddler and migration we're using sqlite
|
||||
meddler.Default = meddler.SQLite
|
||||
migrate.Driver = migrate.SQLite
|
||||
|
||||
// connect to the SQLite database
|
||||
db, err := sql.Open(driver, datasource)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
database.Set(db)
|
||||
|
||||
migration := migrate.New(db)
|
||||
migration.All().Migrate()
|
||||
}
|
||||
|
||||
// discardOldBuilds sets builds that are in the 'Started'
|
||||
// state to 'Failure' on startup. The assumption is that
|
||||
// the drone process was shut down mid-build and thus the
|
||||
|
@ -177,11 +150,18 @@ func setupHandlers() {
|
|||
|
||||
// handlers for setting up your GitHub repository
|
||||
m.Post("/new/github.com", handler.UserHandler(handler.RepoCreateGithub))
|
||||
m.Get("/new/github.com", handler.UserHandler(handler.RepoAdd))
|
||||
m.Get("/new/github.com", handler.UserHandler(handler.RepoAddGithub))
|
||||
|
||||
// handlers for setting up your Bitbucket repository
|
||||
m.Post("/new/bitbucket.org", handler.UserHandler(handler.RepoCreateBitbucket))
|
||||
m.Get("/new/bitbucket.org", handler.UserHandler(handler.RepoAddBitbucket))
|
||||
|
||||
// handlers for linking your GitHub account
|
||||
m.Get("/auth/login/github", handler.UserHandler(handler.LinkGithub))
|
||||
|
||||
// handlers for linking your Bitbucket account
|
||||
m.Get("/auth/login/bitbucket", handler.UserHandler(handler.LinkBitbucket))
|
||||
|
||||
// handlers for dashboard pages
|
||||
m.Get("/dashboard/team/:team", handler.UserHandler(handler.TeamShow))
|
||||
m.Get("/dashboard", handler.UserHandler(handler.UserShow))
|
||||
|
@ -220,7 +200,10 @@ func setupHandlers() {
|
|||
m.Get("/account/admin/users", handler.AdminHandler(handler.AdminUserList))
|
||||
|
||||
// handlers for GitHub post-commit hooks
|
||||
m.Post("/hook/github.com", handler.ErrorHandler(hookHandler.Hook))
|
||||
m.Post("/hook/github.com", handler.ErrorHandler(hookHandler.HookGithub))
|
||||
|
||||
// handlers for Bitbucket post-commit hooks
|
||||
m.Post("/hook/bitbucket.org", handler.ErrorHandler(hookHandler.HookBitbucket))
|
||||
|
||||
// handlers for first-time installation
|
||||
m.Get("/install", handler.ErrorHandler(handler.Install))
|
||||
|
|
|
@ -174,16 +174,31 @@ func (b *Builder) setup() error {
|
|||
// start all services required for the build
|
||||
// that will get linked to the container.
|
||||
for _, service := range b.Build.Services {
|
||||
image, ok := services[service]
|
||||
if !ok {
|
||||
return fmt.Errorf("Error: Invalid or unknown service %s", service)
|
||||
|
||||
// Parse the name of the Docker image
|
||||
// And then construct a fully qualified image name
|
||||
owner, name, tag := parseImageName(service)
|
||||
cname := fmt.Sprintf("%s/%s:%s", owner, name, tag)
|
||||
|
||||
// Get the image info
|
||||
img, err := b.dockerClient.Images.Inspect(cname)
|
||||
if err != nil {
|
||||
// Get the image if it doesn't exist
|
||||
if err := b.dockerClient.Images.Pull(cname); err != nil {
|
||||
return fmt.Errorf("Error: Unable to pull image %s", cname)
|
||||
}
|
||||
|
||||
img, err = b.dockerClient.Images.Inspect(cname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: Invalid or unknown image %s", cname)
|
||||
}
|
||||
}
|
||||
|
||||
// debugging
|
||||
log.Infof("starting service container %s", image.Tag)
|
||||
log.Infof("starting service container %s", cname)
|
||||
|
||||
// Run the contianer
|
||||
run, err := b.dockerClient.Containers.RunDaemonPorts(image.Tag, image.Ports...)
|
||||
run, err := b.dockerClient.Containers.RunDaemonPorts(cname, img.Config.ExposedPorts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -201,7 +216,6 @@ func (b *Builder) setup() error {
|
|||
|
||||
// Add the running service to the list
|
||||
b.services = append(b.services, info)
|
||||
|
||||
}
|
||||
|
||||
if err := b.writeIdentifyFile(dir); err != nil {
|
||||
|
@ -319,13 +333,12 @@ func (b *Builder) run() error {
|
|||
|
||||
// link service containers
|
||||
for i, service := range b.services {
|
||||
image, ok := services[b.Build.Services[i]]
|
||||
if !ok {
|
||||
continue // THIS SHOULD NEVER HAPPEN
|
||||
}
|
||||
// convert name of the image to a slug
|
||||
_, name, _ := parseImageName(b.Build.Services[i])
|
||||
|
||||
// link the service container to our
|
||||
// build container.
|
||||
host.Links = append(host.Links, service.Name[1:]+":"+image.Name)
|
||||
host.Links = append(host.Links, service.Name[1:]+":"+name)
|
||||
}
|
||||
|
||||
// where are temp files going to go?
|
||||
|
@ -478,6 +491,14 @@ func (b *Builder) writeBuildScript(dir string) error {
|
|||
f.WriteEnv("DRONE_PR", b.Repo.PR)
|
||||
f.WriteEnv("DRONE_BUILD_DIR", b.Repo.Dir)
|
||||
|
||||
// add environment variables for code coverage
|
||||
// systems, like coveralls.
|
||||
f.WriteEnv("CI_NAME", "DRONE")
|
||||
f.WriteEnv("CI_BUILD_NUMBER", b.Repo.Commit)
|
||||
f.WriteEnv("CI_BUILD_URL", "")
|
||||
f.WriteEnv("CI_BRANCH", b.Repo.Branch)
|
||||
f.WriteEnv("CI_PULL_REQUEST", b.Repo.PR)
|
||||
|
||||
// add /etc/hosts entries
|
||||
for _, mapping := range b.Build.Hosts {
|
||||
f.WriteHost(mapping)
|
||||
|
|
|
@ -3,6 +3,7 @@ package build
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -108,20 +109,23 @@ func TestSetupEmptyImage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestSetupUnknownService will test our ability to handle an
|
||||
// unknown or unsupported service (i.e. mysql).
|
||||
func TestSetupUnknownService(t *testing.T) {
|
||||
b := Builder{}
|
||||
b.Repo = &repo.Repo{}
|
||||
b.Repo.Path = "git://github.com/drone/drone.git"
|
||||
b.Build = &script.Build{}
|
||||
b.Build.Image = "go1.2"
|
||||
b.Build.Services = append(b.Build.Services, "not-found")
|
||||
// TestSetupErrorInspectImage will test our ability to handle a
|
||||
// failure when inspecting an image (i.e. bradrydzewski/mysql:latest),
|
||||
// which should trigger a `docker pull`.
|
||||
func TestSetupErrorInspectImage(t *testing.T) {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
// TestSetupErrorPullImage will test our ability to handle a
|
||||
// failure when pulling an image (i.e. bradrydzewski/mysql:latest)
|
||||
func TestSetupErrorPullImage(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v1.9/images/bradrydzewski/mysql:5.5/json", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
|
||||
var got, want = b.setup(), "Error: Invalid or unknown service not-found"
|
||||
if got == nil || got.Error() != want {
|
||||
t.Errorf("Expected error %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetupErrorRunDaemonPorts will test our ability to handle a
|
||||
|
@ -130,6 +134,11 @@ func TestSetupErrorRunDaemonPorts(t *testing.T) {
|
|||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v1.9/images/bradrydzewski/mysql:5.5/json", func(w http.ResponseWriter, r *http.Request) {
|
||||
data := []byte(`{"config": { "ExposedPorts": { "6379/tcp": {}}}}`)
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/v1.9/containers/create", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
|
@ -155,6 +164,11 @@ func TestSetupErrorServiceInspect(t *testing.T) {
|
|||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v1.9/images/bradrydzewski/mysql:5.5/json", func(w http.ResponseWriter, r *http.Request) {
|
||||
data := []byte(`{"config": { "ExposedPorts": { "6379/tcp": {}}}}`)
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/v1.9/containers/create", func(w http.ResponseWriter, r *http.Request) {
|
||||
body := `{ "Id":"e90e34656806", "Warnings":[] }`
|
||||
w.Write([]byte(body))
|
||||
|
@ -188,12 +202,11 @@ func TestSetupErrorImagePull(t *testing.T) {
|
|||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v1.9/images/bradrydzewski/go:1.2/json", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/v1.9/images/bradrydzewski/mysql:5.5/json", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/v1.9/images/create", func(w http.ResponseWriter, r *http.Request) {
|
||||
// validate ?fromImage=bradrydzewski/go&tag=1.2
|
||||
mux.HandleFunc("/v1.9/images/create?fromImage=bradrydzewski/mysql&tag=5.5", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
|
||||
|
@ -202,10 +215,11 @@ func TestSetupErrorImagePull(t *testing.T) {
|
|||
b.Repo.Path = "git://github.com/drone/drone.git"
|
||||
b.Build = &script.Build{}
|
||||
b.Build.Image = "go1.2"
|
||||
b.Build.Services = append(b.Build.Services, "mysql")
|
||||
b.dockerClient = client
|
||||
|
||||
var got, want = b.setup(), docker.ErrBadRequest
|
||||
if got == nil || got != want {
|
||||
var got, want = b.setup(), fmt.Errorf("Error: Unable to pull image bradrydzewski/mysql:5.5")
|
||||
if got == nil || got.Error() != want.Error() {
|
||||
t.Errorf("Expected error %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
@ -548,6 +562,11 @@ func TestWriteBuildScript(t *testing.T) {
|
|||
f.WriteEnv("DRONE_COMMIT", "e7e046b35")
|
||||
f.WriteEnv("DRONE_PR", "123")
|
||||
f.WriteEnv("DRONE_BUILD_DIR", "/var/cache/drone/github.com/drone/drone")
|
||||
f.WriteEnv("CI_NAME", "DRONE")
|
||||
f.WriteEnv("CI_BUILD_NUMBER", "e7e046b35")
|
||||
f.WriteEnv("CI_BUILD_URL", "")
|
||||
f.WriteEnv("CI_BRANCH", "master")
|
||||
f.WriteEnv("CI_PULL_REQUEST", "123")
|
||||
f.WriteHost("127.0.0.1")
|
||||
f.WriteCmd("git clone --depth=0 --recursive --branch=master git://github.com/drone/drone.git /var/cache/drone/github.com/drone/drone")
|
||||
f.WriteCmd("git fetch origin +refs/pull/123/head:refs/remotes/origin/pr/123")
|
||||
|
|
|
@ -127,19 +127,18 @@ func (c *ContainerService) RunDaemon(conf *Config, host *HostConfig) (*Run, erro
|
|||
return run, err
|
||||
}
|
||||
|
||||
func (c *ContainerService) RunDaemonPorts(image string, ports ...string) (*Run, error) {
|
||||
func (c *ContainerService) RunDaemonPorts(image string, ports map[Port]struct{}) (*Run, error) {
|
||||
// setup configuration
|
||||
config := Config{Image: image}
|
||||
config.ExposedPorts = make(map[Port]struct{})
|
||||
config.ExposedPorts = ports
|
||||
|
||||
// host configuration
|
||||
host := HostConfig{}
|
||||
host.PortBindings = make(map[Port][]PortBinding)
|
||||
|
||||
// loop through and add ports
|
||||
for _, port := range ports {
|
||||
config.ExposedPorts[Port(port+"/tcp")] = struct{}{}
|
||||
host.PortBindings[Port(port+"/tcp")] = []PortBinding{{HostIp: "127.0.0.1", HostPort: ""}}
|
||||
for port, _ := range ports {
|
||||
host.PortBindings[port] = []PortBinding{{HostIp: "127.0.0.1", HostPort: ""}}
|
||||
}
|
||||
//127.0.0.1::%s
|
||||
//map[3306/tcp:{}] map[3306/tcp:[{127.0.0.1 }]]
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto/sha1"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// createUID is a helper function that will
|
||||
|
@ -26,3 +27,57 @@ func createRandom() []byte {
|
|||
}
|
||||
return k
|
||||
}
|
||||
|
||||
// list of service aliases and their full, canonical names
|
||||
var defaultServices = map[string]string{
|
||||
"cassandra": "relateiq/cassandra:latest",
|
||||
"couchdb": "bradrydzewski/couchdb:1.5",
|
||||
"elasticsearch": "bradrydzewski/elasticsearch:0.90",
|
||||
"memcached": "bradrydzewski/memcached",
|
||||
"mongodb": "bradrydzewski/mongodb:2.4",
|
||||
"mysql": "bradrydzewski/mysql:5.5",
|
||||
"neo4j": "bradrydzewski/neo4j:1.9",
|
||||
"postgres": "bradrydzewski/postgres:9.1",
|
||||
"redis": "bradrydzewski/redis:2.8",
|
||||
"rabbitmq": "bradrydzewski/rabbitmq:3.2",
|
||||
"riak": "guillermo/riak:latest",
|
||||
"zookeeper": "jplock/zookeeper:3.4.5",
|
||||
}
|
||||
|
||||
// parseImageName parses a Docker image name, in the format owner/name:tag,
|
||||
// and returns each segment.
|
||||
//
|
||||
// If the owner is blank, it is assumed to be an official drone image,
|
||||
// and will be prefixed with the appropriate owner name.
|
||||
//
|
||||
// If the tag is empty, it is assumed to be the latest version.
|
||||
func parseImageName(image string) (owner, name, tag string) {
|
||||
owner = "bradrydzewski" // this will eventually change to drone
|
||||
name = image
|
||||
tag = "latest"
|
||||
|
||||
// first we check to see if the image name is an alias
|
||||
// for a known service.
|
||||
//
|
||||
// TODO I'm not a huge fan of this code here. Maybe it
|
||||
// should get handled when the yaml is parsed, and
|
||||
// convert the image and service names in the yaml
|
||||
// to fully qualified names?
|
||||
if cname, ok := defaultServices[image]; ok {
|
||||
name = cname
|
||||
}
|
||||
|
||||
parts := strings.Split(name, "/")
|
||||
if len(parts) == 2 {
|
||||
owner = parts[0]
|
||||
name = parts[1]
|
||||
}
|
||||
|
||||
parts = strings.Split(name, ":")
|
||||
if len(parts) == 2 {
|
||||
name = parts[0]
|
||||
tag = parts[1]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
36
pkg/build/util_test.go
Normal file
36
pkg/build/util_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package build
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseImageName(t *testing.T) {
|
||||
images := []struct {
|
||||
owner string
|
||||
name string
|
||||
tag string
|
||||
cname string
|
||||
}{
|
||||
// full image name with all 3 sections present
|
||||
{"johnsmith", "redis", "2.8", "johnsmith/redis:2.8"},
|
||||
// image name with no tag specified
|
||||
{"johnsmith", "redis", "latest", "johnsmith/redis"},
|
||||
// image name with no owner specified
|
||||
{"bradrydzewski", "redis", "2.8", "redis:2.8"},
|
||||
// image name with ownly name specified
|
||||
{"bradrydzewski", "redis2", "latest", "redis2"},
|
||||
// image name that is a known alias
|
||||
{"relateiq", "cassandra", "latest", "cassandra"},
|
||||
}
|
||||
|
||||
for _, img := range images {
|
||||
owner, name, tag := parseImageName(img.cname)
|
||||
if owner != img.owner {
|
||||
t.Errorf("Expected image %s with owner %s, got %s", img.cname, img.owner, owner)
|
||||
}
|
||||
if name != img.name {
|
||||
t.Errorf("Expected image %s with name %s, got %s", img.cname, img.name, name)
|
||||
}
|
||||
if tag != img.tag {
|
||||
t.Errorf("Expected image %s with tag %s, got %s", img.cname, img.tag, tag)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ SELECT id, repo_id, status, started, finished, duration,
|
|||
hash, branch, pull_request, author, gravatar, timestamp, message, created, updated
|
||||
FROM commits
|
||||
WHERE repo_id = ? AND branch = ?
|
||||
ORDER BY created DESC
|
||||
ORDER BY created DESC, id DESC
|
||||
LIMIT 10
|
||||
`
|
||||
|
||||
|
@ -26,7 +26,7 @@ SELECT id, repo_id, status, started, finished, duration,
|
|||
hash, branch, pull_request, author, gravatar, timestamp, message, created, updated
|
||||
FROM commits
|
||||
WHERE repo_id = ? AND branch = ?
|
||||
ORDER BY created DESC
|
||||
ORDER BY created DESC, id DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
|
@ -57,7 +57,7 @@ WHERE r.user_id = ?
|
|||
AND r.team_id = 0
|
||||
AND r.id = c.repo_id
|
||||
AND c.status IN ('Success', 'Failure')
|
||||
ORDER BY c.created desc
|
||||
ORDER BY c.created desc, c.id desc
|
||||
LIMIT 10
|
||||
`
|
||||
|
||||
|
@ -70,7 +70,7 @@ FROM repos r, commits c
|
|||
WHERE r.team_id = ?
|
||||
AND r.id = c.repo_id
|
||||
AND c.status IN ('Success', 'Failure')
|
||||
ORDER BY c.created desc
|
||||
ORDER BY c.created desc, c.id desc
|
||||
LIMIT 10
|
||||
`
|
||||
|
||||
|
|
|
@ -2,23 +2,62 @@ package database
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"fmt"
|
||||
|
||||
"github.com/drone/drone/pkg/database/schema"
|
||||
"github.com/drone/drone/pkg/database/migrate"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/russross/meddler"
|
||||
)
|
||||
|
||||
// global instance of our database connection.
|
||||
var db *sql.DB
|
||||
|
||||
// Set sets the default database.
|
||||
func Set(database *sql.DB) {
|
||||
// set the global database
|
||||
db = database
|
||||
|
||||
// load the database schema. If this is
|
||||
// a new database all the tables and
|
||||
// indexes will be created.
|
||||
if err := schema.Load(db); err != nil {
|
||||
log.Fatal(err)
|
||||
// Init connects to database and performs migration if necessary.
|
||||
//
|
||||
// Database driver name and data source information is provided by user
|
||||
// from within command line, and error checking is deferred to sql.Open.
|
||||
//
|
||||
// Init will just bail out and returns error if driver name
|
||||
// is not listed, no fallback nor default driver sets here.
|
||||
func Init(name, datasource string) error {
|
||||
var err error
|
||||
driver := map[string]struct {
|
||||
Md *meddler.Database
|
||||
Mg migrate.DriverBuilder
|
||||
}{
|
||||
"sqlite3": {
|
||||
meddler.SQLite,
|
||||
migrate.SQLite,
|
||||
},
|
||||
"mysql": {
|
||||
meddler.MySQL,
|
||||
migrate.MySQL,
|
||||
},
|
||||
}
|
||||
|
||||
if drv, ok := driver[name]; ok {
|
||||
meddler.Default = drv.Md
|
||||
migrate.Driver = drv.Mg
|
||||
} else {
|
||||
return fmt.Errorf("%s driver not found", name)
|
||||
}
|
||||
|
||||
db, err = sql.Open(name, datasource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migration := migrate.New(db)
|
||||
if err := migration.All().Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close database connection.
|
||||
func Close() {
|
||||
db.Close()
|
||||
}
|
||||
|
|
153
pkg/database/migrate/1_setup_tables.go
Normal file
153
pkg/database/migrate/1_setup_tables.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
package migrate
|
||||
|
||||
type rev1st struct{}
|
||||
|
||||
var SetupTables = &rev1st{}
|
||||
|
||||
func (r *rev1st) Revision() int64 {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (r *rev1st) Up(mg *MigrationDriver) error {
|
||||
t := mg.T
|
||||
if _, err := mg.CreateTable("users", []string{
|
||||
t.Integer("id", PRIMARYKEY, AUTOINCREMENT),
|
||||
t.String("email", UNIQUE),
|
||||
t.String("password"),
|
||||
t.String("token", UNIQUE),
|
||||
t.String("name"),
|
||||
t.String("gravatar"),
|
||||
t.Timestamp("created"),
|
||||
t.Timestamp("updated"),
|
||||
t.Bool("admin"),
|
||||
t.String("github_login"),
|
||||
t.String("github_token"),
|
||||
t.String("bitbucket_login"),
|
||||
t.String("bitbucket_token"),
|
||||
t.String("bitbucket_secret"),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.CreateTable("teams", []string{
|
||||
t.Integer("id", PRIMARYKEY, AUTOINCREMENT),
|
||||
t.String("slug", UNIQUE),
|
||||
t.String("name"),
|
||||
t.String("email"),
|
||||
t.String("gravatar"),
|
||||
t.Timestamp("created"),
|
||||
t.Timestamp("updated"),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.CreateTable("members", []string{
|
||||
t.Integer("id", PRIMARYKEY, AUTOINCREMENT),
|
||||
t.Integer("team_id"),
|
||||
t.Integer("user_id"),
|
||||
t.String("role"),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.CreateTable("repos", []string{
|
||||
t.Integer("id", PRIMARYKEY, AUTOINCREMENT),
|
||||
t.String("slug", UNIQUE),
|
||||
t.String("host"),
|
||||
t.String("owner"),
|
||||
t.String("name"),
|
||||
t.Bool("private"),
|
||||
t.Bool("disabled"),
|
||||
t.Bool("disabled_pr"),
|
||||
t.Bool("priveleged"),
|
||||
t.Integer("timeout"),
|
||||
t.Varchar("scm", 25),
|
||||
t.Varchar("url", 1024),
|
||||
t.String("username"),
|
||||
t.String("password"),
|
||||
t.Varchar("public_key", 1024),
|
||||
t.Varchar("private_key", 1024),
|
||||
t.Blob("params"),
|
||||
t.Timestamp("created"),
|
||||
t.Timestamp("updated"),
|
||||
t.Integer("user_id"),
|
||||
t.Integer("team_id"),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.CreateTable("commits", []string{
|
||||
t.Integer("id", PRIMARYKEY, AUTOINCREMENT),
|
||||
t.Integer("repo_id"),
|
||||
t.String("status"),
|
||||
t.Timestamp("started"),
|
||||
t.Timestamp("finished"),
|
||||
t.Integer("duration"),
|
||||
t.Integer("attempts"),
|
||||
t.String("hash"),
|
||||
t.String("branch"),
|
||||
t.String("pull_request"),
|
||||
t.String("author"),
|
||||
t.String("gravatar"),
|
||||
t.String("timestamp"),
|
||||
t.String("message"),
|
||||
t.Timestamp("created"),
|
||||
t.Timestamp("updated"),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.CreateTable("builds", []string{
|
||||
t.Integer("id", PRIMARYKEY, AUTOINCREMENT),
|
||||
t.Integer("commit_id"),
|
||||
t.String("slug"),
|
||||
t.String("status"),
|
||||
t.Timestamp("started"),
|
||||
t.Timestamp("finished"),
|
||||
t.Integer("duration"),
|
||||
t.Timestamp("created"),
|
||||
t.Timestamp("updated"),
|
||||
t.Text("stdout"),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := mg.CreateTable("settings", []string{
|
||||
t.Integer("id", PRIMARYKEY, AUTOINCREMENT),
|
||||
t.String("github_key"),
|
||||
t.String("github_secret"),
|
||||
t.String("bitbucket_key"),
|
||||
t.String("bitbucket_secret"),
|
||||
t.Varchar("smtp_server", 1024),
|
||||
t.Varchar("smtp_port", 5),
|
||||
t.Varchar("smtp_address", 1024),
|
||||
t.Varchar("smtp_username", 1024),
|
||||
t.Varchar("smtp_password", 1024),
|
||||
t.Varchar("hostname", 1024),
|
||||
t.Varchar("scheme", 5),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rev1st) Down(mg *MigrationDriver) error {
|
||||
if _, err := mg.DropTable("settings"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropTable("builds"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropTable("commits"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropTable("repos"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropTable("members"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropTable("teams"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := mg.DropTable("users")
|
||||
return err
|
||||
}
|
|
@ -8,15 +8,15 @@ func (r *Rev1) Revision() int64 {
|
|||
return 201402200603
|
||||
}
|
||||
|
||||
func (r *Rev1) Up(op Operation) error {
|
||||
_, err := op.RenameColumns("repos", map[string]string{
|
||||
func (r *Rev1) Up(mg *MigrationDriver) error {
|
||||
_, err := mg.RenameColumns("repos", map[string]string{
|
||||
"priveleged": "privileged",
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Rev1) Down(op Operation) error {
|
||||
_, err := op.RenameColumns("repos", map[string]string{
|
||||
func (r *Rev1) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.RenameColumns("repos", map[string]string{
|
||||
"privileged": "priveleged",
|
||||
})
|
||||
return err
|
||||
|
|
|
@ -8,19 +8,19 @@ func (r *Rev3) Revision() int64 {
|
|||
return 201402211147
|
||||
}
|
||||
|
||||
func (r *Rev3) Up(op Operation) error {
|
||||
_, err := op.AddColumn("settings", "github_domain VARCHAR(255)")
|
||||
func (r *Rev3) Up(mg *MigrationDriver) error {
|
||||
_, err := mg.AddColumn("settings", "github_domain VARCHAR(255)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = op.AddColumn("settings", "github_apiurl VARCHAR(255)")
|
||||
_, err = mg.AddColumn("settings", "github_apiurl VARCHAR(255)")
|
||||
|
||||
op.Exec("update settings set github_domain=?", "github.com")
|
||||
op.Exec("update settings set github_apiurl=?", "https://api.github.com")
|
||||
mg.Tx.Exec("update settings set github_domain=?", "github.com")
|
||||
mg.Tx.Exec("update settings set github_apiurl=?", "https://api.github.com")
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Rev3) Down(op Operation) error {
|
||||
_, err := op.DropColumns("settings", []string{"github_domain", "github_apiurl"})
|
||||
func (r *Rev3) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.DropColumns("settings", "github_domain", "github_apiurl")
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package migrate
|
||||
|
||||
type rev20140310104446 struct{}
|
||||
|
||||
var AddOpenInvitationColumn = &rev20140310104446{}
|
||||
|
||||
func (r *rev20140310104446) Revision() int64 {
|
||||
return 20140310104446
|
||||
}
|
||||
|
||||
func (r *rev20140310104446) Up(mg *MigrationDriver) error {
|
||||
// Suppress error here for backward compatibility
|
||||
_, err := mg.AddColumn("settings", "open_invitations BOOLEAN")
|
||||
_, err = mg.Tx.Exec("UPDATE settings SET open_invitations=0 WHERE open_invitations IS NULL")
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rev20140310104446) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.DropColumns("settings", "open_invitations")
|
||||
return err
|
||||
}
|
83
pkg/database/migrate/2_setup_indices.go
Normal file
83
pkg/database/migrate/2_setup_indices.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package migrate
|
||||
|
||||
type rev2nd struct{}
|
||||
|
||||
var SetupIndices = &rev2nd{}
|
||||
|
||||
func (r *rev2nd) Revision() int64 {
|
||||
return 2
|
||||
}
|
||||
|
||||
func (r *rev2nd) Up(mg *MigrationDriver) error {
|
||||
if _, err := mg.AddIndex("members", []string{"team_id", "user_id"}, "unique"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.AddIndex("members", []string{"team_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.AddIndex("members", []string{"user_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.AddIndex("commits", []string{"repo_id", "hash", "branch"}, "unique"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.AddIndex("commits", []string{"repo_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.AddIndex("commits", []string{"repo_id", "branch"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.AddIndex("repos", []string{"team_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.AddIndex("repos", []string{"user_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := mg.AddIndex("builds", []string{"commit_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := mg.AddIndex("builds", []string{"commit_id", "slug"})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rev2nd) Down(mg *MigrationDriver) error {
|
||||
if _, err := mg.DropIndex("builds", []string{"commit_id", "slug"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropIndex("builds", []string{"commit_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropIndex("repos", []string{"user_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropIndex("repos", []string{"team_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropIndex("commits", []string{"repo_id", "branch"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropIndex("commits", []string{"repo_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropIndex("commits", []string{"repo_id", "hash", "branch"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropIndex("members", []string{"user_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := mg.DropIndex("members", []string{"team_id"}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := mg.DropIndex("members", []string{"team_id", "user_id"})
|
||||
return err
|
||||
}
|
|
@ -1,10 +1,17 @@
|
|||
package migrate
|
||||
|
||||
// All is called to collect all migration scripts
|
||||
// and adds them to Revision list. New Revision
|
||||
// should be added here ordered by its revision
|
||||
// number.
|
||||
func (m *Migration) All() *Migration {
|
||||
|
||||
// List all migrations here
|
||||
m.Add(SetupTables)
|
||||
m.Add(SetupIndices)
|
||||
m.Add(RenamePrivelegedToPrivileged)
|
||||
m.Add(GitHubEnterpriseSupport)
|
||||
m.Add(AddOpenInvitationColumn)
|
||||
|
||||
// m.Add(...)
|
||||
// ...
|
||||
|
|
63
pkg/database/migrate/api.go
Normal file
63
pkg/database/migrate/api.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// Operation interface covers basic migration operations.
|
||||
// Implementation details is specific for each database,
|
||||
// see migrate/sqlite.go for implementation reference.
|
||||
type Operation interface {
|
||||
|
||||
// CreateTable may be used to create a table named `tableName`
|
||||
// with its columns specification listed in `args` as an array of string
|
||||
CreateTable(tableName string, args []string) (sql.Result, error)
|
||||
|
||||
// RenameTable simply rename table from `tableName` to `newName`
|
||||
RenameTable(tableName, newName string) (sql.Result, error)
|
||||
|
||||
// DropTable drops table named `tableName`
|
||||
DropTable(tableName string) (sql.Result, error)
|
||||
|
||||
// AddColumn adds single new column to `tableName`, columnSpec is
|
||||
// a standard column definition (column name included) which may looks like this:
|
||||
//
|
||||
// mg.AddColumn("example", "email VARCHAR(255) UNIQUE")
|
||||
//
|
||||
// it's equivalent to:
|
||||
//
|
||||
// mg.AddColumn("example", mg.T.String("email", UNIQUE))
|
||||
//
|
||||
AddColumn(tableName, columnSpec string) (sql.Result, error)
|
||||
|
||||
// ChangeColumn may be used to change the type of a column
|
||||
// `newType` should always specify the column's new type even
|
||||
// if the type is not meant to be change. Eg.
|
||||
//
|
||||
// mg.ChangeColumn("example", "name", "VARCHAR(255) UNIQUE")
|
||||
//
|
||||
ChangeColumn(tableName, columnName, newType string) (sql.Result, error)
|
||||
|
||||
// DropColumns drops a list of columns
|
||||
DropColumns(tableName string, columnsToDrop ...string) (sql.Result, error)
|
||||
|
||||
// RenameColumns will rename columns listed in `columnChanges`
|
||||
RenameColumns(tableName string, columnChanges map[string]string) (sql.Result, error)
|
||||
|
||||
// AddIndex adds index on `tableName` indexed by `columns`
|
||||
AddIndex(tableName string, columns []string, flags ...string) (sql.Result, error)
|
||||
|
||||
// DropIndex drops index indexed by `columns` from `tableName`
|
||||
DropIndex(tableName string, columns []string) (sql.Result, error)
|
||||
}
|
||||
|
||||
// MigrationDriver drives migration script by injecting transaction object (*sql.Tx),
|
||||
// `Operation` implementation and column type helper.
|
||||
type MigrationDriver struct {
|
||||
Operation
|
||||
T *columnType
|
||||
Tx *sql.Tx
|
||||
}
|
||||
|
||||
// DriverBuilder is a constructor for MigrationDriver
|
||||
type DriverBuilder func(tx *sql.Tx) *MigrationDriver
|
96
pkg/database/migrate/column_type.go
Normal file
96
pkg/database/migrate/column_type.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
UNIQUE int = iota
|
||||
PRIMARYKEY
|
||||
AUTOINCREMENT
|
||||
NULL
|
||||
NOTNULL
|
||||
)
|
||||
|
||||
// columnType will be injected to migration script
|
||||
// along with MigrationDriver. `AttrMap` is used to
|
||||
// defines distinct column's attribute between database
|
||||
// implementation. e.g. 'AUTOINCREMENT' in sqlite and
|
||||
// 'AUTO_INCREMENT' in mysql.
|
||||
type columnType struct {
|
||||
Driver string
|
||||
AttrMap map[int]string
|
||||
}
|
||||
|
||||
// defaultMap defines default values for column's attribute
|
||||
// lookup.
|
||||
var defaultMap = map[int]string{
|
||||
UNIQUE: "UNIQUE",
|
||||
PRIMARYKEY: "PRIMARY KEY",
|
||||
AUTOINCREMENT: "AUTOINCREMENT",
|
||||
NULL: "NULL",
|
||||
NOTNULL: "NOT NULL",
|
||||
}
|
||||
|
||||
// Integer returns column definition for INTEGER typed column.
|
||||
// Additional attributes may be specified as string or predefined key
|
||||
// listed in defaultMap.
|
||||
func (c *columnType) Integer(colName string, spec ...interface{}) string {
|
||||
return fmt.Sprintf("%s INTEGER %s", colName, c.parseAttr(spec))
|
||||
}
|
||||
|
||||
// String returns column definition for VARCHAR(255) typed column.
|
||||
func (c *columnType) String(colName string, spec ...interface{}) string {
|
||||
return fmt.Sprintf("%s VARCHAR(255) %s", colName, c.parseAttr(spec))
|
||||
}
|
||||
|
||||
// Text returns column definition for TEXT typed column.
|
||||
func (c *columnType) Text(colName string, spec ...interface{}) string {
|
||||
return fmt.Sprintf("%s TEXT %s", colName, c.parseAttr(spec))
|
||||
}
|
||||
|
||||
// Blob returns column definition for BLOB typed column
|
||||
func (c *columnType) Blob(colName string, spec ...interface{}) string {
|
||||
return fmt.Sprintf("%s BLOB %s", colName, c.parseAttr(spec))
|
||||
}
|
||||
|
||||
// Timestamp returns column definition for TIMESTAMP typed column
|
||||
func (c *columnType) Timestamp(colName string, spec ...interface{}) string {
|
||||
return fmt.Sprintf("%s TIMESTAMP %s", colName, c.parseAttr(spec))
|
||||
}
|
||||
|
||||
// Bool returns column definition for BOOLEAN typed column
|
||||
func (c *columnType) Bool(colName string, spec ...interface{}) string {
|
||||
return fmt.Sprintf("%s BOOLEAN %s", colName, c.parseAttr(spec))
|
||||
}
|
||||
|
||||
// Varchar returns column definition for VARCHAR typed column.
|
||||
// column's max length is specified as `length`.
|
||||
func (c *columnType) Varchar(colName string, length int, spec ...interface{}) string {
|
||||
return fmt.Sprintf("%s VARCHAR(%d) %s", colName, length, c.parseAttr(spec))
|
||||
}
|
||||
|
||||
// attr returns string representation of column attribute specified as key for defaultMap.
|
||||
func (c *columnType) attr(flag int) string {
|
||||
if v, ok := c.AttrMap[flag]; ok {
|
||||
return v
|
||||
}
|
||||
return defaultMap[flag]
|
||||
}
|
||||
|
||||
// parseAttr reflects spec value for its type and returns the string
|
||||
// representation returned by `attr`
|
||||
func (c *columnType) parseAttr(spec []interface{}) string {
|
||||
var attrs []string
|
||||
for _, v := range spec {
|
||||
switch reflect.ValueOf(v).Kind() {
|
||||
case reflect.Int:
|
||||
attrs = append(attrs, c.attr(v.(int)))
|
||||
case reflect.String:
|
||||
attrs = append(attrs, v.(string))
|
||||
}
|
||||
}
|
||||
return strings.Join(attrs, " ")
|
||||
}
|
|
@ -1,24 +1,3 @@
|
|||
// Usage
|
||||
// migrate.To(2)
|
||||
// .Add(Version_1)
|
||||
// .Add(Version_2)
|
||||
// .Add(Version_3)
|
||||
// .Exec(db)
|
||||
//
|
||||
// migrate.ToLatest()
|
||||
// .Add(Version_1)
|
||||
// .Add(Version_2)
|
||||
// .Add(Version_3)
|
||||
// .SetDialect(migrate.MySQL)
|
||||
// .Exec(db)
|
||||
//
|
||||
// migrate.ToLatest()
|
||||
// .Add(Version_1)
|
||||
// .Add(Version_2)
|
||||
// .Add(Version_3)
|
||||
// .Backup(path)
|
||||
// .Exec()
|
||||
|
||||
package migrate
|
||||
|
||||
import (
|
||||
|
@ -28,7 +7,7 @@ import (
|
|||
|
||||
const migrationTableStmt = `
|
||||
CREATE TABLE IF NOT EXISTS migration (
|
||||
revision NUMBER PRIMARY KEY
|
||||
revision BIGINT PRIMARY KEY
|
||||
)
|
||||
`
|
||||
|
||||
|
@ -49,45 +28,18 @@ const deleteRevisionStmt = `
|
|||
DELETE FROM migration where revision = ?
|
||||
`
|
||||
|
||||
// Operation interface covers basic migration operations.
|
||||
// Implementation details is specific for each database,
|
||||
// see migrate/sqlite.go for implementation reference.
|
||||
type Operation interface {
|
||||
CreateTable(tableName string, args []string) (sql.Result, error)
|
||||
|
||||
RenameTable(tableName, newName string) (sql.Result, error)
|
||||
|
||||
DropTable(tableName string) (sql.Result, error)
|
||||
|
||||
AddColumn(tableName, columnSpec string) (sql.Result, error)
|
||||
|
||||
DropColumns(tableName string, columnsToDrop []string) (sql.Result, error)
|
||||
|
||||
RenameColumns(tableName string, columnChanges map[string]string) (sql.Result, error)
|
||||
|
||||
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||
|
||||
Query(query string, args ...interface{}) (*sql.Rows, error)
|
||||
|
||||
QueryRow(query string, args ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
type Revision interface {
|
||||
Up(op Operation) error
|
||||
Down(op Operation) error
|
||||
Up(mg *MigrationDriver) error
|
||||
Down(mg *MigrationDriver) error
|
||||
Revision() int64
|
||||
}
|
||||
|
||||
type MigrationDriver struct {
|
||||
Tx *sql.Tx
|
||||
}
|
||||
|
||||
type Migration struct {
|
||||
db *sql.DB
|
||||
revs []Revision
|
||||
}
|
||||
|
||||
var Driver func(tx *sql.Tx) Operation
|
||||
var Driver DriverBuilder
|
||||
|
||||
func New(db *sql.DB) *Migration {
|
||||
return &Migration{db: db}
|
||||
|
@ -99,7 +51,7 @@ func (m *Migration) Add(rev ...Revision) *Migration {
|
|||
return m
|
||||
}
|
||||
|
||||
// Execute the full list of migrations.
|
||||
// Migrate executes the full list of migrations.
|
||||
func (m *Migration) Migrate() error {
|
||||
var target int64
|
||||
if len(m.revs) > 0 {
|
||||
|
@ -111,7 +63,7 @@ func (m *Migration) Migrate() error {
|
|||
return m.MigrateTo(target)
|
||||
}
|
||||
|
||||
// Execute all database migration until
|
||||
// MigrateTo executes all database migration until
|
||||
// you are at the specified revision number.
|
||||
// If the revision number is less than the
|
||||
// current revision, then we will downgrade.
|
||||
|
@ -148,14 +100,14 @@ func (m *Migration) up(target, current int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
op := Driver(tx)
|
||||
mg := Driver(tx)
|
||||
|
||||
// loop through and execute revisions
|
||||
for _, rev := range m.revs {
|
||||
if rev.Revision() > current && rev.Revision() <= target {
|
||||
current = rev.Revision()
|
||||
// execute the revision Upgrade.
|
||||
if err := rev.Up(op); err != nil {
|
||||
if err := rev.Up(mg); err != nil {
|
||||
log.Printf("Failed to upgrade to Revision Number %v\n", current)
|
||||
log.Println(err)
|
||||
return tx.Rollback()
|
||||
|
@ -181,7 +133,7 @@ func (m *Migration) down(target, current int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
op := Driver(tx)
|
||||
mg := Driver(tx)
|
||||
|
||||
// reverse the list of revisions
|
||||
revs := []Revision{}
|
||||
|
@ -195,7 +147,7 @@ func (m *Migration) down(target, current int64) error {
|
|||
if rev.Revision() > target {
|
||||
current = rev.Revision()
|
||||
// execute the revision Upgrade.
|
||||
if err := rev.Down(op); err != nil {
|
||||
if err := rev.Down(mg); err != nil {
|
||||
log.Printf("Failed to downgrade from Revision Number %v\n", current)
|
||||
log.Println(err)
|
||||
return tx.Rollback()
|
||||
|
|
|
@ -9,6 +9,24 @@ titleize() {
|
|||
echo "$1" | sed -r -e "s/-|_/ /g" -e 's/\b(.)/\U\1/g' -e 's/ //g'
|
||||
}
|
||||
|
||||
howto() {
|
||||
echo "Usage:"
|
||||
echo " ./migration create_sample_table"
|
||||
echo ""
|
||||
echo "Above invocation will create a migration script called:"
|
||||
echo " ${REV}_create_sample_table.go"
|
||||
echo "You can add your migration step at the Up and Down function"
|
||||
echo "definition inside the file."
|
||||
echo ""
|
||||
echo "Database transaction available through MigrationDriver,"
|
||||
echo "so you can access mg.Tx (sql.Tx instance) directly,"
|
||||
echo "there are also some migration helpers available, see api.go"
|
||||
echo "for the list of available helpers (Operation interface)."
|
||||
echo ""
|
||||
}
|
||||
|
||||
[[ $# -eq 0 ]] && howto && exit 0
|
||||
|
||||
cat > ${REV}_$filename.go << EOF
|
||||
package migrate
|
||||
|
||||
|
@ -20,11 +38,11 @@ func (r *rev$REV) Revision() int64 {
|
|||
${TAB}return $REV
|
||||
}
|
||||
|
||||
func (r *rev$REV) Up(op Operation) error {
|
||||
func (r *rev$REV) Up(mg *MigrationDriver) error {
|
||||
${TAB}// Migration steps here
|
||||
}
|
||||
|
||||
func (r *rev$REV) Down(op Operation) error {
|
||||
func (r *rev$REV) Down(mg *MigrationDriver) error {
|
||||
${TAB}// Revert migration steps here
|
||||
}
|
||||
EOF
|
||||
|
|
113
pkg/database/migrate/mysql.go
Normal file
113
pkg/database/migrate/mysql.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type mysqlDriver struct {
|
||||
Tx *sql.Tx
|
||||
}
|
||||
|
||||
func MySQL(tx *sql.Tx) *MigrationDriver {
|
||||
return &MigrationDriver{
|
||||
Tx: tx,
|
||||
Operation: &mysqlDriver{Tx: tx},
|
||||
T: &columnType{
|
||||
AttrMap: map[int]string{AUTOINCREMENT: "AUTO_INCREMENT"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mysqlDriver) CreateTable(tableName string, args []string) (sql.Result, error) {
|
||||
return m.Tx.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (%s) ROW_FORMAT=DYNAMIC",
|
||||
tableName, strings.Join(args, ", ")))
|
||||
}
|
||||
|
||||
func (m *mysqlDriver) RenameTable(tableName, newName string) (sql.Result, error) {
|
||||
return m.Tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO %s", tableName, newName))
|
||||
}
|
||||
|
||||
func (m *mysqlDriver) DropTable(tableName string) (sql.Result, error) {
|
||||
return m.Tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName))
|
||||
}
|
||||
|
||||
func (m *mysqlDriver) AddColumn(tableName, columnSpec string) (sql.Result, error) {
|
||||
return m.Tx.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN (%s)", tableName, columnSpec))
|
||||
}
|
||||
|
||||
func (m *mysqlDriver) ChangeColumn(tableName, columnName, newSpecs string) (sql.Result, error) {
|
||||
return m.Tx.Exec(fmt.Sprintf("ALTER TABLE %s MODIFY %s %s", tableName, columnName, newSpecs))
|
||||
}
|
||||
|
||||
func (m *mysqlDriver) DropColumns(tableName string, columnsToDrop ...string) (sql.Result, error) {
|
||||
if len(columnsToDrop) == 0 {
|
||||
return nil, fmt.Errorf("No columns to drop.")
|
||||
}
|
||||
for k, v := range columnsToDrop {
|
||||
columnsToDrop[k] = fmt.Sprintf("DROP %s", v)
|
||||
}
|
||||
return m.Tx.Exec(fmt.Sprintf("ALTER TABLE %s %s", tableName, strings.Join(columnsToDrop, ", ")))
|
||||
}
|
||||
|
||||
func (m *mysqlDriver) RenameColumns(tableName string, columnChanges map[string]string) (sql.Result, error) {
|
||||
var columns []string
|
||||
|
||||
tableSQL, err := m.getTableDefinition(tableName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columns, err = fetchColumns(tableSQL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var colspec []string
|
||||
for k, v := range columnChanges {
|
||||
for _, col := range columns {
|
||||
col = strings.Trim(col, " \n")
|
||||
cols := strings.SplitN(col, " ", 2)
|
||||
if quote(k) == cols[0] {
|
||||
colspec = append(colspec, fmt.Sprintf("CHANGE %s %s %s", k, v, cols[1]))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m.Tx.Exec(fmt.Sprintf("ALTER TABLE %s %s", tableName, strings.Join(colspec, ", ")))
|
||||
}
|
||||
|
||||
func (m *mysqlDriver) AddIndex(tableName string, columns []string, flags ...string) (sql.Result, error) {
|
||||
flag := ""
|
||||
if len(flags) > 0 {
|
||||
switch strings.ToUpper(flags[0]) {
|
||||
case "UNIQUE":
|
||||
fallthrough
|
||||
case "FULLTEXT":
|
||||
fallthrough
|
||||
case "SPATIAL":
|
||||
flag = flags[0]
|
||||
}
|
||||
}
|
||||
return m.Tx.Exec(fmt.Sprintf("CREATE %s INDEX %s ON %s (%s)", flag,
|
||||
indexName(tableName, columns), tableName, strings.Join(columns, ", ")))
|
||||
}
|
||||
|
||||
func (m *mysqlDriver) DropIndex(tableName string, columns []string) (sql.Result, error) {
|
||||
return m.Tx.Exec(fmt.Sprintf("DROP INDEX %s on %s", indexName(tableName, columns), tableName))
|
||||
}
|
||||
|
||||
func (m *mysqlDriver) getTableDefinition(tableName string) (string, error) {
|
||||
var name, def string
|
||||
st := fmt.Sprintf("SHOW CREATE TABLE %s", tableName)
|
||||
if err := m.Tx.QueryRow(st).Scan(&name, &def); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return def, nil
|
||||
}
|
||||
|
||||
func quote(name string) string {
|
||||
return fmt.Sprintf("`%s`", name)
|
||||
}
|
53
pkg/database/migrate/postgresql.go
Normal file
53
pkg/database/migrate/postgresql.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type postgresqlDriver struct {
|
||||
Tx *sql.Tx
|
||||
}
|
||||
|
||||
func PostgreSQL(tx *sql.Tx) *MigrationDriver {
|
||||
return &MigrationDriver{
|
||||
Tx: tx,
|
||||
Operation: &postgresqlDriver{Tx: tx},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *postgresqlDriver) CreateTable(tableName string, args []string) (sql.Result, error) {
|
||||
return nil, errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (p *postgresqlDriver) RenameTable(tableName, newName string) (sql.Result, error) {
|
||||
return nil, errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (p *postgresqlDriver) DropTable(tableName string) (sql.Result, error) {
|
||||
return nil, errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (p *postgresqlDriver) AddColumn(tableName, columnSpec string) (sql.Result, error) {
|
||||
return nil, errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (p *postgresqlDriver) ChangeColumn(tableName, columnName, newSpecs string) (sql.Result, error) {
|
||||
return nil, errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (p *postgresqlDriver) DropColumns(tableName string, columnsToDrop ...string) (sql.Result, error) {
|
||||
return nil, errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (p *postgresqlDriver) RenameColumns(tableName string, columnChanges map[string]string) (sql.Result, error) {
|
||||
return nil, errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (p *postgresqlDriver) AddIndex(tableName string, columns []string, flags ...string) (sql.Result, error) {
|
||||
return nil, errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (p *postgresqlDriver) DropIndex(tableName string, columns []string) (sql.Result, error) {
|
||||
return nil, errors.New("not implemented yet")
|
||||
}
|
|
@ -4,46 +4,94 @@ import (
|
|||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type SQLiteDriver MigrationDriver
|
||||
|
||||
func SQLite(tx *sql.Tx) Operation {
|
||||
return &SQLiteDriver{Tx: tx}
|
||||
type sqliteDriver struct {
|
||||
Tx *sql.Tx
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||
return s.Tx.Exec(query, args...)
|
||||
func SQLite(tx *sql.Tx) *MigrationDriver {
|
||||
return &MigrationDriver{
|
||||
Tx: tx,
|
||||
Operation: &sqliteDriver{Tx: tx},
|
||||
T: &columnType{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) Query(query string, args ...interface{}) (*sql.Rows, error) {
|
||||
return s.Tx.Query(query, args...)
|
||||
func (s *sqliteDriver) CreateTable(tableName string, args []string) (sql.Result, error) {
|
||||
return s.Tx.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (%s)", tableName, strings.Join(args, ", ")))
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) QueryRow(query string, args ...interface{}) *sql.Row {
|
||||
return s.Tx.QueryRow(query, args...)
|
||||
func (s *sqliteDriver) RenameTable(tableName, newName string) (sql.Result, error) {
|
||||
return s.Tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO %s", tableName, newName))
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) CreateTable(tableName string, args []string) (sql.Result, error) {
|
||||
return s.Tx.Exec(fmt.Sprintf("CREATE TABLE %s (%s);", tableName, strings.Join(args, ", ")))
|
||||
func (s *sqliteDriver) DropTable(tableName string) (sql.Result, error) {
|
||||
return s.Tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName))
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) RenameTable(tableName, newName string) (sql.Result, error) {
|
||||
return s.Tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO %s;", tableName, newName))
|
||||
func (s *sqliteDriver) AddColumn(tableName, columnSpec string) (sql.Result, error) {
|
||||
return s.Tx.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", tableName, columnSpec))
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) DropTable(tableName string) (sql.Result, error) {
|
||||
return s.Tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s;", tableName))
|
||||
func (s *sqliteDriver) ChangeColumn(tableName, columnName, newType string) (sql.Result, error) {
|
||||
var result sql.Result
|
||||
var err error
|
||||
|
||||
tableSQL, err := s.getTableDefinition(tableName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columns, err := fetchColumns(tableSQL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columnNames := selectName(columns)
|
||||
|
||||
for k, column := range columnNames {
|
||||
if columnName == column {
|
||||
columns[k] = fmt.Sprintf("%s %s", columnName, newType)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
indices, err := s.getIndexDefinition(tableName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := proxyName(tableName)
|
||||
if result, err = s.RenameTable(tableName, proxy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result, err = s.CreateTable(tableName, columns); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Migrate data
|
||||
if result, err = s.Tx.Exec(fmt.Sprintf("INSERT INTO %s SELECT %s FROM %s", tableName,
|
||||
strings.Join(columnNames, ", "), proxy)); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Clean up proxy table
|
||||
if result, err = s.DropTable(proxy); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
for _, idx := range indices {
|
||||
if result, err = s.Tx.Exec(idx); err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) AddColumn(tableName, columnSpec string) (sql.Result, error) {
|
||||
return s.Tx.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s;", tableName, columnSpec))
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) DropColumns(tableName string, columnsToDrop []string) (sql.Result, error) {
|
||||
func (s *sqliteDriver) DropColumns(tableName string, columnsToDrop ...string) (sql.Result, error) {
|
||||
var err error
|
||||
var result sql.Result
|
||||
|
||||
|
@ -51,7 +99,7 @@ func (s *SQLiteDriver) DropColumns(tableName string, columnsToDrop []string) (sq
|
|||
return nil, fmt.Errorf("No columns to drop.")
|
||||
}
|
||||
|
||||
tableSQL, err := s.getDDLFromTable(tableName)
|
||||
tableSQL, err := s.getTableDefinition(tableName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -82,7 +130,7 @@ func (s *SQLiteDriver) DropColumns(tableName string, columnsToDrop []string) (sq
|
|||
}
|
||||
|
||||
// fetch indices for this table
|
||||
oldSQLIndices, err := s.getDDLFromIndex(tableName)
|
||||
oldSQLIndices, err := s.getIndexDefinition(tableName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -114,8 +162,8 @@ func (s *SQLiteDriver) DropColumns(tableName string, columnsToDrop []string) (sq
|
|||
}
|
||||
|
||||
// Rename old table, here's our proxy
|
||||
proxyName := fmt.Sprintf("%s_%s", tableName, uniuri.NewLen(16))
|
||||
if result, err := s.RenameTable(tableName, proxyName); err != nil {
|
||||
proxy := proxyName(tableName)
|
||||
if result, err := s.RenameTable(tableName, proxy); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
|
@ -125,13 +173,13 @@ func (s *SQLiteDriver) DropColumns(tableName string, columnsToDrop []string) (sq
|
|||
}
|
||||
|
||||
// Move data from old table
|
||||
if result, err = s.Tx.Exec(fmt.Sprintf("INSERT INTO %s SELECT %s FROM %s;", tableName,
|
||||
strings.Join(selectName(preparedColumns), ", "), proxyName)); err != nil {
|
||||
if result, err = s.Tx.Exec(fmt.Sprintf("INSERT INTO %s SELECT %s FROM %s", tableName,
|
||||
strings.Join(selectName(preparedColumns), ", "), proxy)); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Clean up proxy table
|
||||
if result, err = s.DropTable(proxyName); err != nil {
|
||||
if result, err = s.DropTable(proxy); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
|
@ -144,11 +192,11 @@ func (s *SQLiteDriver) DropColumns(tableName string, columnsToDrop []string) (sq
|
|||
return result, err
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) RenameColumns(tableName string, columnChanges map[string]string) (sql.Result, error) {
|
||||
func (s *sqliteDriver) RenameColumns(tableName string, columnChanges map[string]string) (sql.Result, error) {
|
||||
var err error
|
||||
var result sql.Result
|
||||
|
||||
tableSQL, err := s.getDDLFromTable(tableName)
|
||||
tableSQL, err := s.getTableDefinition(tableName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -180,7 +228,7 @@ func (s *SQLiteDriver) RenameColumns(tableName string, columnChanges map[string]
|
|||
}
|
||||
|
||||
// fetch indices for this table
|
||||
oldSQLIndices, err := s.getDDLFromIndex(tableName)
|
||||
oldSQLIndices, err := s.getIndexDefinition(tableName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -214,8 +262,8 @@ func (s *SQLiteDriver) RenameColumns(tableName string, columnChanges map[string]
|
|||
}
|
||||
|
||||
// Rename current table
|
||||
proxyName := fmt.Sprintf("%s_%s", tableName, uniuri.NewLen(16))
|
||||
if result, err := s.RenameTable(tableName, proxyName); err != nil {
|
||||
proxy := proxyName(tableName)
|
||||
if result, err := s.RenameTable(tableName, proxy); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
|
@ -226,12 +274,12 @@ func (s *SQLiteDriver) RenameColumns(tableName string, columnChanges map[string]
|
|||
|
||||
// Migrate data
|
||||
if result, err = s.Tx.Exec(fmt.Sprintf("INSERT INTO %s SELECT %s FROM %s", tableName,
|
||||
strings.Join(oldColumnsName, ", "), proxyName)); err != nil {
|
||||
strings.Join(oldColumnsName, ", "), proxy)); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Clean up proxy table
|
||||
if result, err = s.DropTable(proxyName); err != nil {
|
||||
if result, err = s.DropTable(proxy); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
|
@ -243,9 +291,24 @@ func (s *SQLiteDriver) RenameColumns(tableName string, columnChanges map[string]
|
|||
return result, err
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) getDDLFromTable(tableName string) (string, error) {
|
||||
func (s *sqliteDriver) AddIndex(tableName string, columns []string, flags ...string) (sql.Result, error) {
|
||||
flag := ""
|
||||
if len(flags) > 0 {
|
||||
if strings.ToUpper(flags[0]) == "UNIQUE" {
|
||||
flag = flags[0]
|
||||
}
|
||||
}
|
||||
return s.Tx.Exec(fmt.Sprintf("CREATE %s INDEX %s ON %s (%s)", flag, indexName(tableName, columns),
|
||||
tableName, strings.Join(columns, ", ")))
|
||||
}
|
||||
|
||||
func (s *sqliteDriver) DropIndex(tableName string, columns []string) (sql.Result, error) {
|
||||
return s.Tx.Exec(fmt.Sprintf("DROP INDEX %s", indexName(tableName, columns)))
|
||||
}
|
||||
|
||||
func (s *sqliteDriver) getTableDefinition(tableName string) (string, error) {
|
||||
var sql string
|
||||
query := `SELECT sql FROM sqlite_master WHERE type='table' and name=?;`
|
||||
query := `SELECT sql FROM sqlite_master WHERE type='table' and name=?`
|
||||
err := s.Tx.QueryRow(query, tableName).Scan(&sql)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -253,26 +316,23 @@ func (s *SQLiteDriver) getDDLFromTable(tableName string) (string, error) {
|
|||
return sql, nil
|
||||
}
|
||||
|
||||
func (s *SQLiteDriver) getDDLFromIndex(tableName string) ([]string, error) {
|
||||
func (s *sqliteDriver) getIndexDefinition(tableName string) ([]string, error) {
|
||||
var sqls []string
|
||||
|
||||
query := `SELECT sql FROM sqlite_master WHERE type='index' and tbl_name=?;`
|
||||
query := `SELECT sql FROM sqlite_master WHERE type='index' and tbl_name=?`
|
||||
rows, err := s.Tx.Query(query, tableName)
|
||||
if err != nil {
|
||||
return sqls, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var sql string
|
||||
var sql sql.NullString
|
||||
if err := rows.Scan(&sql); err != nil {
|
||||
// This error came from autoindex, since its sql value is null,
|
||||
// we want to continue.
|
||||
if strings.Contains(err.Error(), "Scan pair: <nil> -> *string") {
|
||||
continue
|
||||
}
|
||||
return sqls, err
|
||||
}
|
||||
sqls = append(sqls, sql)
|
||||
if sql.Valid {
|
||||
sqls = append(sqls, sql.String)
|
||||
}
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package migrate
|
||||
package migrate_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
@ -6,6 +6,9 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/drone/drone/pkg/database/migrate"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/russross/meddler"
|
||||
)
|
||||
|
||||
|
@ -33,8 +36,8 @@ type AddColumnSample struct {
|
|||
|
||||
type revision1 struct{}
|
||||
|
||||
func (r *revision1) Up(op Operation) error {
|
||||
_, err := op.CreateTable("samples", []string{
|
||||
func (r *revision1) Up(mg *MigrationDriver) error {
|
||||
_, err := mg.CreateTable("samples", []string{
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT",
|
||||
"imel VARCHAR(255) UNIQUE",
|
||||
"name VARCHAR(255)",
|
||||
|
@ -42,8 +45,8 @@ func (r *revision1) Up(op Operation) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (r *revision1) Down(op Operation) error {
|
||||
_, err := op.DropTable("samples")
|
||||
func (r *revision1) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.DropTable("samples")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -57,13 +60,13 @@ func (r *revision1) Revision() int64 {
|
|||
|
||||
type revision2 struct{}
|
||||
|
||||
func (r *revision2) Up(op Operation) error {
|
||||
_, err := op.RenameTable("samples", "examples")
|
||||
func (r *revision2) Up(mg *MigrationDriver) error {
|
||||
_, err := mg.RenameTable("samples", "examples")
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *revision2) Down(op Operation) error {
|
||||
_, err := op.RenameTable("examples", "samples")
|
||||
func (r *revision2) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.RenameTable("examples", "samples")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -77,16 +80,16 @@ func (r *revision2) Revision() int64 {
|
|||
|
||||
type revision3 struct{}
|
||||
|
||||
func (r *revision3) Up(op Operation) error {
|
||||
if _, err := op.AddColumn("samples", "url VARCHAR(255)"); err != nil {
|
||||
func (r *revision3) Up(mg *MigrationDriver) error {
|
||||
if _, err := mg.AddColumn("samples", "url VARCHAR(255)"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := op.AddColumn("samples", "num INTEGER")
|
||||
_, err := mg.AddColumn("samples", "num INTEGER")
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *revision3) Down(op Operation) error {
|
||||
_, err := op.DropColumns("samples", []string{"num", "url"})
|
||||
func (r *revision3) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.DropColumns("samples", "num", "url")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -100,15 +103,15 @@ func (r *revision3) Revision() int64 {
|
|||
|
||||
type revision4 struct{}
|
||||
|
||||
func (r *revision4) Up(op Operation) error {
|
||||
_, err := op.RenameColumns("samples", map[string]string{
|
||||
func (r *revision4) Up(mg *MigrationDriver) error {
|
||||
_, err := mg.RenameColumns("samples", map[string]string{
|
||||
"imel": "email",
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *revision4) Down(op Operation) error {
|
||||
_, err := op.RenameColumns("samples", map[string]string{
|
||||
func (r *revision4) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.RenameColumns("samples", map[string]string{
|
||||
"email": "imel",
|
||||
})
|
||||
return err
|
||||
|
@ -124,13 +127,13 @@ func (r *revision4) Revision() int64 {
|
|||
|
||||
type revision5 struct{}
|
||||
|
||||
func (r *revision5) Up(op Operation) error {
|
||||
_, err := op.Exec(`CREATE INDEX samples_url_name_ix ON samples (url, name)`)
|
||||
func (r *revision5) Up(mg *MigrationDriver) error {
|
||||
_, err := mg.AddIndex("samples", []string{"url", "name"})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *revision5) Down(op Operation) error {
|
||||
_, err := op.Exec(`DROP INDEX samples_url_name_ix`)
|
||||
func (r *revision5) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.DropIndex("samples", []string{"url", "name"})
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -143,15 +146,15 @@ func (r *revision5) Revision() int64 {
|
|||
// ---------- revision 6
|
||||
type revision6 struct{}
|
||||
|
||||
func (r *revision6) Up(op Operation) error {
|
||||
_, err := op.RenameColumns("samples", map[string]string{
|
||||
func (r *revision6) Up(mg *MigrationDriver) error {
|
||||
_, err := mg.RenameColumns("samples", map[string]string{
|
||||
"url": "host",
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *revision6) Down(op Operation) error {
|
||||
_, err := op.RenameColumns("samples", map[string]string{
|
||||
func (r *revision6) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.RenameColumns("samples", map[string]string{
|
||||
"host": "url",
|
||||
})
|
||||
return err
|
||||
|
@ -166,16 +169,16 @@ func (r *revision6) Revision() int64 {
|
|||
// ---------- revision 7
|
||||
type revision7 struct{}
|
||||
|
||||
func (r *revision7) Up(op Operation) error {
|
||||
_, err := op.DropColumns("samples", []string{"host", "num"})
|
||||
func (r *revision7) Up(mg *MigrationDriver) error {
|
||||
_, err := mg.DropColumns("samples", "host", "num")
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *revision7) Down(op Operation) error {
|
||||
if _, err := op.AddColumn("samples", "host VARCHAR(255)"); err != nil {
|
||||
func (r *revision7) Down(mg *MigrationDriver) error {
|
||||
if _, err := mg.AddColumn("samples", "host VARCHAR(255)"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := op.AddColumn("samples", "num INSTEGER")
|
||||
_, err := mg.AddColumn("samples", "num INSTEGER")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -188,16 +191,16 @@ func (r *revision7) Revision() int64 {
|
|||
// ---------- revision 8
|
||||
type revision8 struct{}
|
||||
|
||||
func (r *revision8) Up(op Operation) error {
|
||||
if _, err := op.AddColumn("samples", "repo_id INTEGER"); err != nil {
|
||||
func (r *revision8) Up(mg *MigrationDriver) error {
|
||||
if _, err := mg.AddColumn("samples", "repo_id INTEGER"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := op.AddColumn("samples", "repo VARCHAR(255)")
|
||||
_, err := mg.AddColumn("samples", "repo VARCHAR(255)")
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *revision8) Down(op Operation) error {
|
||||
_, err := op.DropColumns("samples", []string{"repo", "repo_id"})
|
||||
func (r *revision8) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.DropColumns("samples", "repo", "repo_id")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -210,15 +213,15 @@ func (r *revision8) Revision() int64 {
|
|||
// ---------- revision 9
|
||||
type revision9 struct{}
|
||||
|
||||
func (r *revision9) Up(op Operation) error {
|
||||
_, err := op.RenameColumns("samples", map[string]string{
|
||||
func (r *revision9) Up(mg *MigrationDriver) error {
|
||||
_, err := mg.RenameColumns("samples", map[string]string{
|
||||
"repo": "repository",
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *revision9) Down(op Operation) error {
|
||||
_, err := op.RenameColumns("samples", map[string]string{
|
||||
func (r *revision9) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.RenameColumns("samples", map[string]string{
|
||||
"repository": "repo",
|
||||
})
|
||||
return err
|
||||
|
@ -230,6 +233,26 @@ func (r *revision9) Revision() int64 {
|
|||
|
||||
// ---------- end of revision 9
|
||||
|
||||
// ---------- revision 10
|
||||
|
||||
type revision10 struct{}
|
||||
|
||||
func (r *revision10) Revision() int64 {
|
||||
return 10
|
||||
}
|
||||
|
||||
func (r *revision10) Up(mg *MigrationDriver) error {
|
||||
_, err := mg.ChangeColumn("samples", "email", "varchar(512) UNIQUE")
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *revision10) Down(mg *MigrationDriver) error {
|
||||
_, err := mg.ChangeColumn("samples", "email", "varchar(255) unique")
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- end of revision 10
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
var testSchema = `
|
||||
|
@ -252,11 +275,9 @@ func TestMigrateCreateTable(t *testing.T) {
|
|||
t.Fatalf("Error preparing database: %q", err)
|
||||
}
|
||||
|
||||
Driver = SQLite
|
||||
|
||||
mgr := New(db)
|
||||
if err := mgr.Add(&revision1{}).Migrate(); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
sample := Sample{
|
||||
|
@ -265,7 +286,30 @@ func TestMigrateCreateTable(t *testing.T) {
|
|||
Name: "Test Tester",
|
||||
}
|
||||
if err := meddler.Save(db, "samples", &sample); err != nil {
|
||||
t.Errorf("Can not save data: %q", err)
|
||||
t.Fatalf("Can not save data: %q", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateExistingCreateTable(t *testing.T) {
|
||||
defer tearDown()
|
||||
if err := setUp(); err != nil {
|
||||
t.Fatalf("Error preparing database: %q", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(testSchema); err != nil {
|
||||
t.Fatalf("Can not create database: %q", err)
|
||||
}
|
||||
|
||||
mgr := New(db)
|
||||
rev := &revision1{}
|
||||
if err := mgr.Add(rev).Migrate(); err != nil {
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
var current int64
|
||||
db.QueryRow("SELECT max(revision) FROM migration").Scan(¤t)
|
||||
if current != rev.Revision() {
|
||||
t.Fatalf("Did not successfully migrate")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -275,22 +319,20 @@ func TestMigrateRenameTable(t *testing.T) {
|
|||
t.Fatalf("Error preparing database: %q", err)
|
||||
}
|
||||
|
||||
Driver = SQLite
|
||||
|
||||
mgr := New(db)
|
||||
if err := mgr.Add(&revision1{}).Migrate(); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
loadFixture(t)
|
||||
|
||||
if err := mgr.Add(&revision2{}).Migrate(); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
sample := Sample{}
|
||||
if err := meddler.QueryRow(db, &sample, `SELECT * FROM examples WHERE id = ?`, 2); err != nil {
|
||||
t.Errorf("Can not fetch data: %q", err)
|
||||
t.Fatalf("Can not fetch data: %q", err)
|
||||
}
|
||||
|
||||
if sample.Imel != "foo@bar.com" {
|
||||
|
@ -313,16 +355,14 @@ func TestMigrateAddRemoveColumns(t *testing.T) {
|
|||
t.Fatalf("Error preparing database: %q", err)
|
||||
}
|
||||
|
||||
Driver = SQLite
|
||||
|
||||
mgr := New(db)
|
||||
if err := mgr.Add(&revision1{}, &revision3{}).Migrate(); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
var columns []*TableInfo
|
||||
if err := meddler.QueryAll(db, &columns, `PRAGMA table_info(samples);`); err != nil {
|
||||
t.Errorf("Can not access table info: %q", err)
|
||||
t.Fatalf("Can not access table info: %q", err)
|
||||
}
|
||||
|
||||
if len(columns) < 5 {
|
||||
|
@ -337,16 +377,16 @@ func TestMigrateAddRemoveColumns(t *testing.T) {
|
|||
Num: 42,
|
||||
}
|
||||
if err := meddler.Save(db, "samples", &row); err != nil {
|
||||
t.Errorf("Can not save into database: %q", err)
|
||||
t.Fatalf("Can not save into database: %q", err)
|
||||
}
|
||||
|
||||
if err := mgr.MigrateTo(1); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
var another_columns []*TableInfo
|
||||
if err := meddler.QueryAll(db, &another_columns, `PRAGMA table_info(samples);`); err != nil {
|
||||
t.Errorf("Can not access table info: %q", err)
|
||||
t.Fatalf("Can not access table info: %q", err)
|
||||
}
|
||||
|
||||
if len(another_columns) != 3 {
|
||||
|
@ -360,22 +400,20 @@ func TestRenameColumn(t *testing.T) {
|
|||
t.Fatalf("Error preparing database: %q", err)
|
||||
}
|
||||
|
||||
Driver = SQLite
|
||||
|
||||
mgr := New(db)
|
||||
if err := mgr.Add(&revision1{}, &revision4{}).MigrateTo(1); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
loadFixture(t)
|
||||
|
||||
if err := mgr.MigrateTo(4); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
row := RenameSample{}
|
||||
if err := meddler.QueryRow(db, &row, `SELECT * FROM samples WHERE id = 3;`); err != nil {
|
||||
t.Errorf("Can not query database: %q", err)
|
||||
t.Fatalf("Can not query database: %q", err)
|
||||
}
|
||||
|
||||
if row.Email != "crash@bandicoot.io" {
|
||||
|
@ -389,22 +427,20 @@ func TestMigrateExistingTable(t *testing.T) {
|
|||
t.Fatalf("Error preparing database: %q", err)
|
||||
}
|
||||
|
||||
Driver = SQLite
|
||||
|
||||
if _, err := db.Exec(testSchema); err != nil {
|
||||
t.Errorf("Can not create database: %q", err)
|
||||
t.Fatalf("Can not create database: %q", err)
|
||||
}
|
||||
|
||||
loadFixture(t)
|
||||
|
||||
mgr := New(db)
|
||||
if err := mgr.Add(&revision4{}).Migrate(); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
var rows []*RenameSample
|
||||
if err := meddler.QueryAll(db, &rows, `SELECT * from samples;`); err != nil {
|
||||
t.Errorf("Can not query database: %q", err)
|
||||
t.Fatalf("Can not query database: %q", err)
|
||||
}
|
||||
|
||||
if len(rows) != 3 {
|
||||
|
@ -426,49 +462,47 @@ func TestIndexOperations(t *testing.T) {
|
|||
t.Fatalf("Error preparing database: %q", err)
|
||||
}
|
||||
|
||||
Driver = SQLite
|
||||
|
||||
mgr := New(db)
|
||||
|
||||
// Migrate, create index
|
||||
if err := mgr.Add(&revision1{}, &revision3{}, &revision5{}).Migrate(); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
var esquel []*sqliteMaster
|
||||
// Query sqlite_master, check if index is exists.
|
||||
query := `SELECT sql FROM sqlite_master WHERE type='index' and tbl_name='samples'`
|
||||
if err := meddler.QueryAll(db, &esquel, query); err != nil {
|
||||
t.Errorf("Can not find index: %q", err)
|
||||
t.Fatalf("Can not find index: %q", err)
|
||||
}
|
||||
|
||||
indexStatement := `CREATE INDEX samples_url_name_ix ON samples (url, name)`
|
||||
indexStatement := `CREATE INDEX idx_samples_on_url_and_name ON samples (url, name)`
|
||||
if string(esquel[1].Sql.([]byte)) != indexStatement {
|
||||
t.Errorf("Can not find index")
|
||||
t.Errorf("Can not find index, got: %q", esquel[1])
|
||||
}
|
||||
|
||||
// Migrate, rename indexed columns
|
||||
if err := mgr.Add(&revision6{}).Migrate(); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
var esquel1 []*sqliteMaster
|
||||
if err := meddler.QueryAll(db, &esquel1, query); err != nil {
|
||||
t.Errorf("Can not find index: %q", err)
|
||||
t.Fatalf("Can not find index: %q", err)
|
||||
}
|
||||
|
||||
indexStatement = `CREATE INDEX samples_host_name_ix ON samples (host, name)`
|
||||
indexStatement = `CREATE INDEX idx_samples_on_host_and_name ON samples (host, name)`
|
||||
if string(esquel1[1].Sql.([]byte)) != indexStatement {
|
||||
t.Errorf("Can not find index, got: %s", esquel[0])
|
||||
t.Errorf("Can not find index, got: %q", esquel1[1])
|
||||
}
|
||||
|
||||
if err := mgr.Add(&revision7{}).Migrate(); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
var esquel2 []*sqliteMaster
|
||||
if err := meddler.QueryAll(db, &esquel2, query); err != nil {
|
||||
t.Errorf("Can not find index: %q", err)
|
||||
t.Fatalf("Can not find index: %q", err)
|
||||
}
|
||||
|
||||
if len(esquel2) != 1 {
|
||||
|
@ -482,17 +516,15 @@ func TestColumnRedundancy(t *testing.T) {
|
|||
t.Fatalf("Error preparing database: %q", err)
|
||||
}
|
||||
|
||||
Driver = SQLite
|
||||
|
||||
migr := New(db)
|
||||
if err := migr.Add(&revision1{}, &revision8{}, &revision9{}).Migrate(); err != nil {
|
||||
t.Errorf("Can not migrate: %q", err)
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
var tableSql string
|
||||
query := `SELECT sql FROM sqlite_master where type='table' and name='samples'`
|
||||
if err := db.QueryRow(query).Scan(&tableSql); err != nil {
|
||||
t.Errorf("Can not query sqlite_master: %q", err)
|
||||
t.Fatalf("Can not query sqlite_master: %q", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(tableSql, "repository ") {
|
||||
|
@ -500,8 +532,31 @@ func TestColumnRedundancy(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChangeColumnType(t *testing.T) {
|
||||
defer tearDown()
|
||||
if err := setUp(); err != nil {
|
||||
t.Fatalf("Error preparing database: %q", err)
|
||||
}
|
||||
|
||||
migr := New(db)
|
||||
if err := migr.Add(&revision1{}, &revision4{}, &revision10{}).Migrate(); err != nil {
|
||||
t.Fatalf("Can not migrate: %q", err)
|
||||
}
|
||||
|
||||
var tableSql string
|
||||
query := `SELECT sql FROM sqlite_master where type='table' and name='samples'`
|
||||
if err := db.QueryRow(query).Scan(&tableSql); err != nil {
|
||||
t.Fatalf("Can not query sqlite_master: %q", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(tableSql, "email varchar(512) UNIQUE") {
|
||||
t.Errorf("Expect email type to changed: %q", tableSql)
|
||||
}
|
||||
}
|
||||
|
||||
func setUp() error {
|
||||
var err error
|
||||
Driver = SQLite
|
||||
db, err = sql.Open("sqlite3", "migration_tests.sqlite")
|
||||
return err
|
||||
}
|
||||
|
@ -514,7 +569,7 @@ func tearDown() {
|
|||
func loadFixture(t *testing.T) {
|
||||
for _, sql := range dataDump {
|
||||
if _, err := db.Exec(sql); err != nil {
|
||||
t.Errorf("Can not insert into database: %q", err)
|
||||
t.Fatalf("Can not insert into database: %q", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package migrate
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
)
|
||||
|
||||
func fetchColumns(sql string) ([]string, error) {
|
||||
|
@ -30,3 +32,11 @@ func setForUpdate(left []string, right []string) string {
|
|||
}
|
||||
return strings.Join(results, ", ")
|
||||
}
|
||||
|
||||
func proxyName(tableName string) string {
|
||||
return fmt.Sprintf("%s_%s", tableName, uniuri.NewLen(16))
|
||||
}
|
||||
|
||||
func indexName(tableName string, columns []string) string {
|
||||
return fmt.Sprintf("idx_%s_on_%s", tableName, strings.Join(columns, "_and_"))
|
||||
}
|
||||
|
|
|
@ -16,31 +16,31 @@ func TestGetCommit(t *testing.T) {
|
|||
}
|
||||
|
||||
if commit.ID != 1 {
|
||||
t.Errorf("Exepected ID %d, got %d", 1, commit.ID)
|
||||
t.Errorf("Expected ID %d, got %d", 1, commit.ID)
|
||||
}
|
||||
|
||||
if commit.Status != "Success" {
|
||||
t.Errorf("Exepected Status %s, got %s", "Success", commit.Status)
|
||||
t.Errorf("Expected Status %s, got %s", "Success", commit.Status)
|
||||
}
|
||||
|
||||
if commit.Hash != "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608" {
|
||||
t.Errorf("Exepected Hash %s, got %s", "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608", commit.Hash)
|
||||
t.Errorf("Expected Hash %s, got %s", "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608", commit.Hash)
|
||||
}
|
||||
|
||||
if commit.Branch != "master" {
|
||||
t.Errorf("Exepected Branch %s, got %s", "master", commit.Branch)
|
||||
t.Errorf("Expected Branch %s, got %s", "master", commit.Branch)
|
||||
}
|
||||
|
||||
if commit.Author != "brad.rydzewski@gmail.com" {
|
||||
t.Errorf("Exepected Author %s, got %s", "master", commit.Author)
|
||||
t.Errorf("Expected Author %s, got %s", "master", commit.Author)
|
||||
}
|
||||
|
||||
if commit.Message != "commit message" {
|
||||
t.Errorf("Exepected Message %s, got %s", "master", commit.Message)
|
||||
t.Errorf("Expected Message %s, got %s", "master", commit.Message)
|
||||
}
|
||||
|
||||
if commit.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
|
||||
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", commit.Gravatar)
|
||||
t.Errorf("Expected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", commit.Gravatar)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,15 +54,15 @@ func TestGetCommitHash(t *testing.T) {
|
|||
}
|
||||
|
||||
if commit.ID != 1 {
|
||||
t.Errorf("Exepected ID %d, got %d", 1, commit.ID)
|
||||
t.Errorf("Expected ID %d, got %d", 1, commit.ID)
|
||||
}
|
||||
|
||||
if commit.Hash != "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608" {
|
||||
t.Errorf("Exepected Hash %s, got %s", "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608", commit.Hash)
|
||||
t.Errorf("Expected Hash %s, got %s", "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608", commit.Hash)
|
||||
}
|
||||
|
||||
if commit.Status != "Success" {
|
||||
t.Errorf("Exepected Status %s, got %s", "Success", commit.Status)
|
||||
t.Errorf("Expected Status %s, got %s", "Success", commit.Status)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,11 +91,11 @@ func TestSaveCommit(t *testing.T) {
|
|||
}
|
||||
|
||||
if commit.Hash != updatedCommit.Hash {
|
||||
t.Errorf("Exepected Hash %s, got %s", updatedCommit.Hash, commit.Hash)
|
||||
t.Errorf("Expected Hash %s, got %s", updatedCommit.Hash, commit.Hash)
|
||||
}
|
||||
|
||||
if commit.Status != "Failing" {
|
||||
t.Errorf("Exepected Status %s, got %s", updatedCommit.Status, commit.Status)
|
||||
t.Errorf("Expected Status %s, got %s", updatedCommit.Status, commit.Status)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,7 +126,7 @@ func TestListCommits(t *testing.T) {
|
|||
|
||||
// verify commit count
|
||||
if len(commits) != 2 {
|
||||
t.Errorf("Exepected %d commits in database, got %d", 2, len(commits))
|
||||
t.Errorf("Expected %d commits in database, got %d", 2, len(commits))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -135,30 +135,30 @@ func TestListCommits(t *testing.T) {
|
|||
commit := commits[1] // TODO something strange is happening with ordering here
|
||||
|
||||
if commit.ID != 1 {
|
||||
t.Errorf("Exepected ID %d, got %d", 1, commit.ID)
|
||||
t.Errorf("Expected ID %d, got %d", 1, commit.ID)
|
||||
}
|
||||
|
||||
if commit.Status != "Success" {
|
||||
t.Errorf("Exepected Status %s, got %s", "Success", commit.Status)
|
||||
t.Errorf("Expected Status %s, got %s", "Success", commit.Status)
|
||||
}
|
||||
|
||||
if commit.Hash != "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608" {
|
||||
t.Errorf("Exepected Hash %s, got %s", "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608", commit.Hash)
|
||||
t.Errorf("Expected Hash %s, got %s", "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608", commit.Hash)
|
||||
}
|
||||
|
||||
if commit.Branch != "master" {
|
||||
t.Errorf("Exepected Branch %s, got %s", "master", commit.Branch)
|
||||
t.Errorf("Expected Branch %s, got %s", "master", commit.Branch)
|
||||
}
|
||||
|
||||
if commit.Author != "brad.rydzewski@gmail.com" {
|
||||
t.Errorf("Exepected Author %s, got %s", "master", commit.Author)
|
||||
t.Errorf("Expected Author %s, got %s", "master", commit.Author)
|
||||
}
|
||||
|
||||
if commit.Message != "commit message" {
|
||||
t.Errorf("Exepected Message %s, got %s", "master", commit.Message)
|
||||
t.Errorf("Expected Message %s, got %s", "master", commit.Message)
|
||||
}
|
||||
|
||||
if commit.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
|
||||
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", commit.Gravatar)
|
||||
t.Errorf("Expected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", commit.Gravatar)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,21 +64,21 @@ func TestIsMemberAdmin(t *testing.T) {
|
|||
if ok, err := database.IsMemberAdmin(1, 1); err != nil {
|
||||
t.Error(err)
|
||||
} else if !ok {
|
||||
t.Errorf("Expected IsMemberAdmin to return true, returned false")
|
||||
t.Errorf("Expected user id 1 IsMemberAdmin to return true, returned false")
|
||||
}
|
||||
|
||||
// expecting user is Admin
|
||||
if ok, err := database.IsMemberAdmin(2, 1); err != nil {
|
||||
t.Error(err)
|
||||
} else if !ok {
|
||||
t.Errorf("Expected IsMemberAdmin to return true, returned false")
|
||||
t.Errorf("Expected user id 2 IsMemberAdmin to return true, returned false")
|
||||
}
|
||||
|
||||
// expecting user is NOT Admin (Write role)
|
||||
if ok, err := database.IsMemberAdmin(3, 1); err != nil {
|
||||
t.Error(err)
|
||||
} else if ok {
|
||||
t.Errorf("Expected IsMemberAdmin to return false, returned true")
|
||||
t.Errorf("Expected user id 3 IsMemberAdmin to return false, returned true")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,22 +2,16 @@ package database
|
|||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"database/sql"
|
||||
"log"
|
||||
|
||||
"github.com/drone/drone/pkg/database"
|
||||
"github.com/drone/drone/pkg/database/encrypt"
|
||||
"github.com/drone/drone/pkg/database/migrate"
|
||||
. "github.com/drone/drone/pkg/model"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/russross/meddler"
|
||||
)
|
||||
|
||||
// in-memory database used for
|
||||
// unit testing purposes.
|
||||
var db *sql.DB
|
||||
|
||||
func init() {
|
||||
// create a cipher for ecnrypting and decrypting
|
||||
// database fields
|
||||
|
@ -30,20 +24,11 @@ func init() {
|
|||
// decrypt database fields.
|
||||
meddler.Register("gobencrypt", &encrypt.EncryptedField{cipher})
|
||||
|
||||
// notify meddler that we are working with sqlite
|
||||
meddler.Default = meddler.SQLite
|
||||
migrate.Driver = migrate.SQLite
|
||||
}
|
||||
|
||||
func Setup() {
|
||||
// create an in-memory database
|
||||
db, _ = sql.Open("sqlite3", ":memory:")
|
||||
|
||||
// make sure all the tables and indexes are created
|
||||
database.Set(db)
|
||||
|
||||
migration := migrate.New(db)
|
||||
migration.All().Migrate()
|
||||
database.Init("sqlite3", ":memory:")
|
||||
|
||||
// create dummy user data
|
||||
user1 := User{
|
||||
|
@ -208,5 +193,5 @@ func Setup() {
|
|||
}
|
||||
|
||||
func Teardown() {
|
||||
db.Close()
|
||||
database.Close()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
. "github.com/drone/drone/pkg/model"
|
||||
"github.com/drone/go-github/github"
|
||||
"github.com/drone/go-github/oauth2"
|
||||
"github.com/drone/go-bitbucket/bitbucket"
|
||||
"github.com/drone/go-bitbucket/oauth1"
|
||||
)
|
||||
|
||||
// Create the User session.
|
||||
|
@ -94,3 +96,78 @@ func LinkGithub(w http.ResponseWriter, r *http.Request, u *User) error {
|
|||
http.Redirect(w, r, "/new/github.com", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
||||
func LinkBitbucket(w http.ResponseWriter, r *http.Request, u *User) error {
|
||||
|
||||
// get settings from database
|
||||
settings := database.SettingsMust()
|
||||
|
||||
// bitbucket oauth1 consumer
|
||||
var consumer = oauth1.Consumer{
|
||||
RequestTokenURL: "https://bitbucket.org/api/1.0/oauth/request_token/",
|
||||
AuthorizationURL: "https://bitbucket.org/!api/1.0/oauth/authenticate",
|
||||
AccessTokenURL: "https://bitbucket.org/api/1.0/oauth/access_token/",
|
||||
CallbackURL: settings.URL().String() + "/auth/login/bitbucket",
|
||||
ConsumerKey: settings.BitbucketKey,
|
||||
ConsumerSecret: settings.BitbucketSecret,
|
||||
}
|
||||
|
||||
// get the oauth verifier
|
||||
verifier := r.FormValue("oauth_verifier")
|
||||
if len(verifier) == 0 {
|
||||
// Generate a Request Token
|
||||
requestToken, err := consumer.RequestToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add the request token as a signed cookie
|
||||
SetCookie(w, r, "bitbucket_token", requestToken.Encode())
|
||||
|
||||
url, _ := consumer.AuthorizeRedirect(requestToken)
|
||||
http.Redirect(w, r, url, http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
||||
// remove bitbucket token data once before redirecting
|
||||
// back to the application.
|
||||
defer DelCookie(w, r, "bitbucket_token")
|
||||
|
||||
// get the tokens from the request
|
||||
requestTokenStr := GetCookie(r, "bitbucket_token")
|
||||
requestToken, err := oauth1.ParseRequestTokenStr(requestTokenStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// exchange for an access token
|
||||
accessToken, err := consumer.AuthorizeToken(requestToken, verifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create the Bitbucket client
|
||||
client := bitbucket.New(
|
||||
settings.BitbucketKey,
|
||||
settings.BitbucketSecret,
|
||||
accessToken.Token(),
|
||||
accessToken.Secret(),
|
||||
)
|
||||
|
||||
// get the currently authenticated Bitbucket User
|
||||
user, err := client.Users.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update the user account
|
||||
u.BitbucketLogin = user.User.Username
|
||||
u.BitbucketToken = accessToken.Token()
|
||||
u.BitbucketSecret = accessToken.Secret()
|
||||
if err := database.SaveUser(u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/new/bitbucket.org", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
. "github.com/drone/drone/pkg/model"
|
||||
"github.com/drone/drone/pkg/queue"
|
||||
"github.com/drone/go-github/github"
|
||||
"github.com/drone/go-bitbucket/bitbucket"
|
||||
)
|
||||
|
||||
type HookHandler struct {
|
||||
|
@ -23,9 +24,9 @@ func NewHookHandler(queue *queue.Queue) *HookHandler {
|
|||
}
|
||||
}
|
||||
|
||||
// Processes a generic POST-RECEIVE hook and
|
||||
// Processes a generic POST-RECEIVE GitHub hook and
|
||||
// attempts to trigger a build.
|
||||
func (h *HookHandler) Hook(w http.ResponseWriter, r *http.Request) error {
|
||||
func (h *HookHandler) HookGithub(w http.ResponseWriter, r *http.Request) error {
|
||||
// handle github ping
|
||||
if r.Header.Get("X-Github-Event") == "ping" {
|
||||
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
|
||||
|
@ -34,7 +35,7 @@ func (h *HookHandler) Hook(w http.ResponseWriter, r *http.Request) error {
|
|||
// if this is a pull request route
|
||||
// to a different handler
|
||||
if r.Header.Get("X-Github-Event") == "pull_request" {
|
||||
h.PullRequestHook(w, r)
|
||||
h.PullRequestHookGithub(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -175,7 +176,7 @@ func (h *HookHandler) Hook(w http.ResponseWriter, r *http.Request) error {
|
|||
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *HookHandler) PullRequestHook(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *HookHandler) PullRequestHookGithub(w http.ResponseWriter, r *http.Request) {
|
||||
// get the payload of the message
|
||||
// this should contain a json representation of the
|
||||
// repository and commit details
|
||||
|
@ -291,6 +292,99 @@ func (h *HookHandler) PullRequestHook(w http.ResponseWriter, r *http.Request) {
|
|||
RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
|
||||
}
|
||||
|
||||
// Processes a generic POST-RECEIVE Bitbucket hook and
|
||||
// attempts to trigger a build.
|
||||
func (h *HookHandler) HookBitbucket(w http.ResponseWriter, r *http.Request) error {
|
||||
// get the payload from the request
|
||||
payload := r.FormValue("payload")
|
||||
|
||||
// parse the post-commit hook
|
||||
hook, err := bitbucket.ParseHook([]byte(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the repo from the URL
|
||||
repoId := r.FormValue("id")
|
||||
|
||||
// get the repo from the database, return error if not found
|
||||
repo, err := database.GetRepoSlug(repoId)
|
||||
if err != nil {
|
||||
return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
}
|
||||
|
||||
// Get the user that owns the repository
|
||||
user, err := database.GetUser(repo.UserID)
|
||||
if err != nil {
|
||||
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Verify that the commit doesn't already exist.
|
||||
// We should never build the same commit twice.
|
||||
_, err = database.GetCommitHash(hook.Commits[len(hook.Commits)-1].Hash, repo.ID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
|
||||
}
|
||||
|
||||
commit := &Commit{}
|
||||
commit.RepoID = repo.ID
|
||||
commit.Branch = hook.Commits[len(hook.Commits)-1].Branch
|
||||
commit.Hash = hook.Commits[len(hook.Commits)-1].Hash
|
||||
commit.Status = "Pending"
|
||||
commit.Created = time.Now().UTC()
|
||||
commit.Message = hook.Commits[len(hook.Commits)-1].Message
|
||||
commit.Timestamp = time.Now().UTC().String()
|
||||
commit.SetAuthor(hook.Commits[len(hook.Commits)-1].Author)
|
||||
|
||||
// get the github settings from the database
|
||||
settings := database.SettingsMust()
|
||||
|
||||
// create the Bitbucket client
|
||||
client := bitbucket.New(
|
||||
settings.BitbucketKey,
|
||||
settings.BitbucketSecret,
|
||||
user.BitbucketToken,
|
||||
user.BitbucketSecret,
|
||||
)
|
||||
|
||||
// get the yaml from the database
|
||||
raw, err := client.Sources.Find(repo.Owner, repo.Name, commit.Hash, ".drone.yml")
|
||||
if err != nil {
|
||||
return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
}
|
||||
|
||||
// parse the build script
|
||||
buildscript, err := script.ParseBuild([]byte(raw.Data), repo.Params)
|
||||
if err != nil {
|
||||
msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
|
||||
if err := saveFailedBuild(commit, msg); err != nil {
|
||||
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// save the commit to the database
|
||||
if err := database.SaveCommit(commit); err != nil {
|
||||
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// save the build to the database
|
||||
build := &Build{}
|
||||
build.Slug = "1" // TODO
|
||||
build.CommitID = commit.ID
|
||||
build.Created = time.Now().UTC()
|
||||
build.Status = "Pending"
|
||||
if err := database.SaveBuild(build); err != nil {
|
||||
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// send the build to the queue
|
||||
h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
|
||||
|
||||
// OK!
|
||||
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
|
||||
}
|
||||
|
||||
// Helper method for saving a failed build or commit in the case where it never starts to build.
|
||||
// This can happen if the yaml is bad or doesn't exist.
|
||||
func saveFailedBuild(commit *Commit, msg string) error {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/drone/drone/pkg/database"
|
||||
. "github.com/drone/drone/pkg/model"
|
||||
"github.com/drone/go-github/github"
|
||||
"github.com/drone/go-bitbucket/bitbucket"
|
||||
|
||||
"launchpad.net/goyaml"
|
||||
)
|
||||
|
@ -52,7 +53,7 @@ func RepoDashboard(w http.ResponseWriter, r *http.Request, u *User, repo *Repo)
|
|||
return RenderTemplate(w, "repo_dashboard.html", &data)
|
||||
}
|
||||
|
||||
func RepoAdd(w http.ResponseWriter, r *http.Request, u *User) error {
|
||||
func RepoAddGithub(w http.ResponseWriter, r *http.Request, u *User) error {
|
||||
settings := database.SettingsMust()
|
||||
teams, err := database.ListTeams(u.ID)
|
||||
if err != nil {
|
||||
|
@ -73,6 +74,27 @@ func RepoAdd(w http.ResponseWriter, r *http.Request, u *User) error {
|
|||
return RenderTemplate(w, "github_add.html", &data)
|
||||
}
|
||||
|
||||
func RepoAddBitbucket(w http.ResponseWriter, r *http.Request, u *User) error {
|
||||
settings := database.SettingsMust()
|
||||
teams, err := database.ListTeams(u.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := struct {
|
||||
User *User
|
||||
Teams []*Team
|
||||
Settings *Settings
|
||||
}{u, teams, settings}
|
||||
// if the user hasn't linked their Bitbucket account
|
||||
// render a different template
|
||||
if len(u.BitbucketToken) == 0 {
|
||||
return RenderTemplate(w, "bitbucket_link.html", &data)
|
||||
}
|
||||
// otherwise display the template for adding
|
||||
// a new Bitbucket repository.
|
||||
return RenderTemplate(w, "bitbucket_add.html", &data)
|
||||
}
|
||||
|
||||
func RepoCreateGithub(w http.ResponseWriter, r *http.Request, u *User) error {
|
||||
teamName := r.FormValue("team")
|
||||
owner := r.FormValue("owner")
|
||||
|
@ -146,6 +168,84 @@ func RepoCreateGithub(w http.ResponseWriter, r *http.Request, u *User) error {
|
|||
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
|
||||
}
|
||||
|
||||
func RepoCreateBitbucket(w http.ResponseWriter, r *http.Request, u *User) error {
|
||||
teamName := r.FormValue("team")
|
||||
owner := r.FormValue("owner")
|
||||
name := r.FormValue("name")
|
||||
|
||||
// get the bitbucket settings from the database
|
||||
settings := database.SettingsMust()
|
||||
|
||||
// create the Bitbucket client
|
||||
client := bitbucket.New(
|
||||
settings.BitbucketKey,
|
||||
settings.BitbucketSecret,
|
||||
u.BitbucketToken,
|
||||
u.BitbucketSecret,
|
||||
)
|
||||
|
||||
bitbucketRepo, err := client.Repos.Find(owner, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to find Bitbucket repository %s/%s.", owner, name)
|
||||
}
|
||||
|
||||
repo, err := NewBitbucketRepo(owner, name, bitbucketRepo.Private)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo.UserID = u.ID
|
||||
repo.Private = bitbucketRepo.Private
|
||||
|
||||
// if the user chose to assign to a team account
|
||||
// we need to retrieve the team, verify the user
|
||||
// has access, and then set the team id.
|
||||
if len(teamName) > 0 {
|
||||
team, err := database.GetTeamSlug(teamName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to find Team %s.", teamName)
|
||||
}
|
||||
|
||||
// user must be an admin member of the team
|
||||
if ok, _ := database.IsMemberAdmin(u.ID, team.ID); !ok {
|
||||
return fmt.Errorf("Invalid permission to access Team %s.", teamName)
|
||||
}
|
||||
|
||||
repo.TeamID = team.ID
|
||||
}
|
||||
|
||||
// if the repository is private we'll need
|
||||
// to upload a bitbucket key to the repository
|
||||
if repo.Private {
|
||||
// name the key
|
||||
keyName := fmt.Sprintf("%s@%s", repo.Owner, settings.Domain)
|
||||
|
||||
// create the bitbucket key, or update if one already exists
|
||||
_, err := client.RepoKeys.CreateUpdate(owner, name, repo.PublicKey, keyName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to add Public Key to your Bitbucket repository: %s", err)
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
// create a hook so that we get notified when code
|
||||
// is pushed to the repository and can execute a build.
|
||||
link := fmt.Sprintf("%s://%s/hook/bitbucket.org?id=%s", settings.Scheme, settings.Domain, repo.Slug)
|
||||
|
||||
// add the hook
|
||||
if _, err := client.Brokers.CreateUpdate(owner, name, link, bitbucket.BrokerTypePost); err != nil {
|
||||
return fmt.Errorf("Unable to add Hook to your Bitbucket repository. %s", err.Error())
|
||||
}
|
||||
|
||||
// Save to the database
|
||||
if err := database.SaveRepo(repo); err != nil {
|
||||
return fmt.Errorf("Error saving repository to the database. %s", err)
|
||||
}
|
||||
|
||||
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
|
||||
}
|
||||
|
||||
// Repository Settings
|
||||
func RepoSettingsForm(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
|
||||
|
||||
|
|
18
pkg/plugin/deploy/bash.go
Normal file
18
pkg/plugin/deploy/bash.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package deploy
|
||||
|
||||
import (
|
||||
"github.com/drone/drone/pkg/build/buildfile"
|
||||
)
|
||||
|
||||
type Bash struct {
|
||||
Script []string `yaml:"script,omitempty"`
|
||||
Command string `yaml:"command,omitempty"`
|
||||
}
|
||||
|
||||
func (g *Bash) Write(f *buildfile.Buildfile) {
|
||||
g.Script = append(g.Script, g.Command)
|
||||
|
||||
for _, cmd := range g.Script {
|
||||
f.WriteCmd(cmd)
|
||||
}
|
||||
}
|
94
pkg/plugin/deploy/bash_test.go
Normal file
94
pkg/plugin/deploy/bash_test.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package deploy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/pkg/build/buildfile"
|
||||
|
||||
"launchpad.net/goyaml"
|
||||
)
|
||||
|
||||
// emulate Build struct
|
||||
type buildWithBash struct {
|
||||
Deploy *Deploy `yaml:"deploy,omitempty"`
|
||||
}
|
||||
|
||||
var sampleYmlWithBash = `
|
||||
deploy:
|
||||
bash:
|
||||
command: 'echo bash_deployed'
|
||||
`
|
||||
|
||||
var sampleYmlWithScript = `
|
||||
deploy:
|
||||
bash:
|
||||
script:
|
||||
- ./bin/deploy.sh
|
||||
- ./bin/check.sh
|
||||
`
|
||||
|
||||
var sampleYmlWithBashAndScript = `
|
||||
deploy:
|
||||
bash:
|
||||
command: ./bin/some_cmd.sh
|
||||
script:
|
||||
- ./bin/deploy.sh
|
||||
- ./bin/check.sh
|
||||
`
|
||||
|
||||
func setUpWithBash(input string) (string, error) {
|
||||
var buildStruct buildWithBash
|
||||
err := goyaml.Unmarshal([]byte(input), &buildStruct)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
bf := buildfile.New()
|
||||
buildStruct.Deploy.Write(bf)
|
||||
return bf.String(), err
|
||||
}
|
||||
|
||||
func TestBashDeployment(t *testing.T) {
|
||||
bscr, err := setUpWithBash(sampleYmlWithBash)
|
||||
if err != nil {
|
||||
t.Fatalf("Can't unmarshal deploy script: %s", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(bscr, "echo bash_deployed") {
|
||||
t.Error("Expect script to contains bash command")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBashDeploymentWithScript(t *testing.T) {
|
||||
bscr, err := setUpWithBash(sampleYmlWithScript)
|
||||
if err != nil {
|
||||
t.Fatalf("Can't unmarshal deploy script: %s", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(bscr, "./bin/deploy.sh") {
|
||||
t.Error("Expect script to contains bash script")
|
||||
}
|
||||
|
||||
if !strings.Contains(bscr, "./bin/check.sh") {
|
||||
t.Error("Expect script to contains bash script")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBashDeploymentWithBashAndScript(t *testing.T) {
|
||||
bscr, err := setUpWithBash(sampleYmlWithBashAndScript)
|
||||
if err != nil {
|
||||
t.Fatalf("Can't unmarshal deploy script: %s", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(bscr, "./bin/deploy.sh") {
|
||||
t.Error("Expect script to contains bash script")
|
||||
}
|
||||
|
||||
if !strings.Contains(bscr, "./bin/check.sh") {
|
||||
t.Error("Expect script to contains bash script")
|
||||
}
|
||||
|
||||
if !strings.Contains(bscr, "./bin/some_cmd.sh") {
|
||||
t.Error("Expect script to contains bash script")
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ type Deploy struct {
|
|||
Openshift *Openshift `yaml:"openshift,omitempty"`
|
||||
SSH *SSH `yaml:"ssh,omitempty"`
|
||||
Tsuru *Tsuru `yaml:"tsuru,omitempty"`
|
||||
Bash *Bash `yaml:"bash,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Deploy) Write(f *buildfile.Buildfile) {
|
||||
|
@ -55,4 +56,7 @@ func (d *Deploy) Write(f *buildfile.Buildfile) {
|
|||
if d.Tsuru != nil {
|
||||
d.Tsuru.Write(f)
|
||||
}
|
||||
if d.Bash != nil {
|
||||
d.Bash.Write(f)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,14 @@ import (
|
|||
// a Build has succeeded
|
||||
type Publish struct {
|
||||
S3 *S3 `yaml:"s3,omitempty"`
|
||||
Swift *Swift `yaml:"swift,omitempty"`
|
||||
}
|
||||
|
||||
func (p *Publish) Write(f *buildfile.Buildfile) {
|
||||
if p.S3 != nil {
|
||||
p.S3.Write(f)
|
||||
}
|
||||
if p.Swift != nil {
|
||||
p.Swift.Write(f)
|
||||
}
|
||||
}
|
||||
|
|
60
pkg/plugin/publish/swift.go
Normal file
60
pkg/plugin/publish/swift.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package publish
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/drone/drone/pkg/build/buildfile"
|
||||
)
|
||||
|
||||
type Swift struct {
|
||||
// Username for authentication
|
||||
Username string `yaml:"username,omitempty"`
|
||||
|
||||
// Password for authentication
|
||||
// With Rackspace this is usually an API Key
|
||||
Password string `yaml:"password,omitempty"`
|
||||
|
||||
// Container to upload files to
|
||||
Container string `yaml:"container,omitempty"`
|
||||
|
||||
// Base API version URL to authenticate against
|
||||
// Rackspace: https://identity.api.rackspacecloud.com/v2.0
|
||||
AuthURL string `yaml:"auth_url,omitempty"`
|
||||
|
||||
// Region to communicate with, in a generic OpenStack install
|
||||
// this may be RegionOne
|
||||
Region string `yaml:"region,omitempty"`
|
||||
|
||||
// Source file or directory to upload, if source is a directory,
|
||||
// upload the contents of the directory
|
||||
Source string `yaml:"source,omitempty"`
|
||||
|
||||
// Destination to write the file(s) to. Should contain the full
|
||||
// object name if source is a file
|
||||
Target string `yaml:"target,omitempty"`
|
||||
|
||||
Branch string `yaml:"branch,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Swift) Write(f *buildfile.Buildfile) {
|
||||
// All options are required, so ensure they are present
|
||||
if len(s.Username) == 0 || len(s.Password) == 0 || len(s.AuthURL) == 0 || len(s.Region) == 0 || len(s.Source) == 0 || len(s.Container) == 0 || len(s.Target) == 0 {
|
||||
f.WriteCmdSilent(`echo "Swift: Missing argument(s)"`)
|
||||
return
|
||||
}
|
||||
|
||||
// debugging purposes so we can see if / where something is failing
|
||||
f.WriteCmdSilent(`echo "Swift: Publishing..."`)
|
||||
|
||||
// install swiftly using PIP
|
||||
f.WriteCmdSilent("[ -f /usr/bin/sudo ] || pip install swiftly 1> /dev/null 2> /dev/null")
|
||||
f.WriteCmdSilent("[ -f /usr/bin/sudo ] && sudo pip install swiftly 1> /dev/null 2> /dev/null")
|
||||
|
||||
// Write out environment variables
|
||||
f.WriteEnv("SWIFTLY_AUTH_URL", s.AuthURL)
|
||||
f.WriteEnv("SWIFTLY_AUTH_USER", s.Username)
|
||||
f.WriteEnv("SWIFTLY_AUTH_KEY", s.Password)
|
||||
f.WriteEnv("SWIFTLY_REGION", s.Region)
|
||||
|
||||
f.WriteCmd(fmt.Sprintf(`swiftly put -i %s %s/%s`, s.Source, s.Container, s.Target))
|
||||
}
|
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
type BuildRunner interface {
|
||||
Run(buildScript *script.Build, repo *repo.Repo, key []byte, buildOutput io.Writer) (success bool, err error)
|
||||
Run(buildScript *script.Build, repo *repo.Repo, key []byte, privileged bool, buildOutput io.Writer) (success bool, err error)
|
||||
}
|
||||
|
||||
type buildRunner struct {
|
||||
|
@ -26,11 +26,12 @@ func NewBuildRunner(dockerClient *docker.Client, timeout time.Duration) BuildRun
|
|||
}
|
||||
}
|
||||
|
||||
func (runner *buildRunner) Run(buildScript *script.Build, repo *repo.Repo, key []byte, buildOutput io.Writer) (bool, error) {
|
||||
func (runner *buildRunner) Run(buildScript *script.Build, repo *repo.Repo, key []byte, privileged bool, buildOutput io.Writer) (bool, error) {
|
||||
builder := build.New(runner.dockerClient)
|
||||
builder.Build = buildScript
|
||||
builder.Repo = repo
|
||||
builder.Key = key
|
||||
builder.Privileged = privileged
|
||||
builder.Stdout = buildOutput
|
||||
builder.Timeout = runner.timeout
|
||||
|
||||
|
|
|
@ -183,6 +183,7 @@ func (w *worker) runBuild(task *BuildTask, buf io.Writer) (bool, error) {
|
|||
task.Script,
|
||||
repo,
|
||||
[]byte(task.Repo.PrivateKey),
|
||||
task.Repo.Privileged,
|
||||
buf,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -50,12 +50,12 @@
|
|||
<input class="form-control form-control-large" type="text" name="GitHubApiUrl" value="{{.Settings.GitHubApiUrl}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group hide">
|
||||
<div class="form-group">
|
||||
<div class="alert">Bitbucket OAuth Consumer Key and Secret.</div>
|
||||
<label>Bitbucket Key and Secret:</label>
|
||||
<div>
|
||||
<input class="form-control form-control-large" type="text" name="BitbucketKey" value="" />
|
||||
<input class="form-control form-control-large" type="password" name="BitbucketSecret" value="" />
|
||||
<input class="form-control form-control-large" type="text" name="BitbucketKey" value="{{.Settings.BitbucketKey}}" />
|
||||
<input class="form-control form-control-large" type="password" name="BitbucketSecret" value="{{.Settings.BitbucketSecret}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -119,4 +119,4 @@
|
|||
return false;
|
||||
};
|
||||
</script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
|
99
pkg/template/pages/bitbucket_add.html
Normal file
99
pkg/template/pages/bitbucket_add.html
Normal file
|
@ -0,0 +1,99 @@
|
|||
{{ define "title" }}Bitbucket · Add Repository{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="subhead">
|
||||
<div class="container">
|
||||
<h1>
|
||||
<span>Repository Setup</span>
|
||||
<small>Bitbucket</small>
|
||||
</h1>
|
||||
</div><!-- ./container -->
|
||||
</div><!-- ./subhead -->
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li><a href="/new/github.com">GitHub</a></li>
|
||||
<li class="active"><a href="/new/bitbucket.org">Bitbucket</a></li>
|
||||
</ul>
|
||||
</div><!-- ./col-xs-3 -->
|
||||
|
||||
<div class="col-xs-9" role="main">
|
||||
<div class="alert">
|
||||
Enter your repository details
|
||||
<a class="btn btn-default pull-right" href="/auth/login/bitbucket" style="font-size: 18px;background:#f4f4f4;">Re-Link Account</a>
|
||||
</div>
|
||||
<form class="form-repo" method="POST" action="/new/bitbucket.org">
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<label>Bitbucket Owner</label>
|
||||
<div>
|
||||
<input class="form-control form-control-large" type="text" name="owner" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-separator">/</div>
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<label>Repository Name</label>
|
||||
<div>
|
||||
<input class="form-control form-control-large" type="text" name="name" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="alert">Select your Drone account</div>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="radio" name="team" checked="True" value="">
|
||||
<img src="{{ .User.Image }}?s=32">
|
||||
<span>Me</span>
|
||||
</li>
|
||||
{{ range .Teams }}
|
||||
<li>
|
||||
<input type="radio" name="team" value="{{ .Slug }}">
|
||||
<img src="{{ .Image }}?s=32">
|
||||
<span>{{ .Name }}</span>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<div class="alert alert-success hide" id="successAlert"></div>
|
||||
<div class="alert alert-error hide" id="failureAlert"></div>
|
||||
<div class="form-actions">
|
||||
<input class="btn btn-primary" id="submitButton" type="submit" value="Add" data-loading-text="Saving ..">
|
||||
<a class="btn btn-default" href="/dashboard">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- ./col-xs-9 -->
|
||||
</div><!-- ./row -->
|
||||
</div><!-- ./container -->
|
||||
{{ end }}
|
||||
|
||||
{{ define "script" }}
|
||||
<script>
|
||||
document.forms[0].onsubmit = function(event) {
|
||||
$("#successAlert").hide();
|
||||
$("#failureAlert").hide();
|
||||
$('#submitButton').button('loading')
|
||||
|
||||
var form = event.target
|
||||
var formData = new FormData(form);
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', form.action);
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
var name = $("input[name=name]").val()
|
||||
var owner = $("input[name=owner]").val()
|
||||
window.location.pathname = "/bitbucket.org/"+owner+"/"+name
|
||||
} else {
|
||||
$("#failureAlert").text("Unable to setup the Repository");
|
||||
$("#failureAlert").show().removeClass("hide");
|
||||
$('#submitButton').button('reset')
|
||||
};
|
||||
};
|
||||
xhr.send(formData);
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
31
pkg/template/pages/bitbucket_link.html
Normal file
31
pkg/template/pages/bitbucket_link.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
{{ define "title" }}Bitbucket · Add Repository{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="subhead">
|
||||
<div class="container">
|
||||
<h1>
|
||||
<span>Repository Setup</span>
|
||||
<small>Bitbucket</small>
|
||||
</h1>
|
||||
</div><!-- ./container -->
|
||||
</div><!-- ./subhead -->
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li><a href="/new/github.com">GitHub</a></li>
|
||||
<li class="active"><a href="/new/bitbucket.org">Bitbucket</a></li>
|
||||
</ul>
|
||||
</div><!-- ./col-xs-3 -->
|
||||
|
||||
<div class="col-xs-9" role="main">
|
||||
<div class="alert">Link Your Bitbucket Account
|
||||
<a class="btn btn-primary pull-right" href="/auth/login/bitbucket" style="font-size: 18px;">Link Now</a>
|
||||
</div>
|
||||
</div><!-- ./col-xs-9 -->
|
||||
</div><!-- ./row -->
|
||||
</div><!-- ./container -->
|
||||
{{ end }}
|
||||
|
||||
{{ define "script" }}{{ end }}
|
|
@ -15,7 +15,7 @@
|
|||
<div class="col-xs-3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="/new/github.com">GitHub</a></li>
|
||||
<li><a href="/new/bitbucket.org">Bitbucket <small>(coming soon)</small></a></li>
|
||||
<li><a href="/new/bitbucket.org">Bitbucket</a></li>
|
||||
</ul>
|
||||
</div><!-- ./col-xs-3 -->
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div class="col-xs-3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="/new/github.com">GitHub</a></li>
|
||||
<li><a href="/new/bitbucket.org">Bitbucket <small>(coming soon)</small></a></li>
|
||||
<li><a href="/new/bitbucket.org">Bitbucket</a></li>
|
||||
</ul>
|
||||
</div><!-- ./col-xs-3 -->
|
||||
|
||||
|
|
|
@ -82,6 +82,8 @@ func init() {
|
|||
"admin_settings.html",
|
||||
"github_add.html",
|
||||
"github_link.html",
|
||||
"bitbucket_add.html",
|
||||
"bitbucket_link.html",
|
||||
}
|
||||
|
||||
// extract the base template as a string
|
||||
|
|
Loading…
Reference in a new issue