Merge remote-tracking branch 'drone/master' into fail-pending-on-startup

Conflicts:
	cmd/droned/drone.go
This commit is contained in:
Michael Nutt 2014-04-01 15:32:43 -04:00
commit efb5d4e0cf
45 changed files with 1827 additions and 358 deletions

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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))

View file

@ -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)

View file

@ -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")

View file

@ -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 }]]

View file

@ -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
View 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)
}
}
}

View file

@ -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
`

View file

@ -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()
}

View 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
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View 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
}

View file

@ -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(...)
// ...

View 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

View 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, " ")
}

View file

@ -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()

View file

@ -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

View 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)
}

View 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")
}

View file

@ -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 {

View file

@ -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(&current)
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)
}
}
}

View file

@ -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_"))
}

View file

@ -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)
}
}

View file

@ -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")
}
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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
View 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)
}
}

View 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")
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View 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))
}

View file

@ -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

View file

@ -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,
)
}

View file

@ -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 }}

View 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 }}

View 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 }}

View file

@ -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 -->

View file

@ -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 -->

View file

@ -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