{"action":"created","comment":{"url":"https://api.github.com/repos/flynn/flynn/pulls/comments/52498957","id":52498957,"diff_hunk":"@@ -0,0 +1,1106 @@\n+package mariadb\n+\n+import (\n+\t\"database/sql\"\n+\t\"errors\"\n+\t\"fmt\"\n+\t\"io\"\n+\t\"io/ioutil\"\n+\t\"net\"\n+\t\"net/http\"\n+\t\"net/url\"\n+\t\"os\"\n+\t\"os/exec\"\n+\t\"path/filepath\"\n+\t\"strconv\"\n+\t\"strings\"\n+\t\"sync\"\n+\t\"sync/atomic\"\n+\t\"syscall\"\n+\t\"text/template\"\n+\t\"time\"\n+\n+\t_ \"github.com/flynn/flynn/Godeps/_workspace/src/github.com/go-sql-driver/mysql\"\n+\t\"github.com/flynn/flynn/Godeps/_workspace/src/gopkg.in/inconshreveable/log15.v2\"\n+\t\"github.com/flynn/flynn/discoverd/client\"\n+\t\"github.com/flynn/flynn/pkg/shutdown\"\n+\t\"github.com/flynn/flynn/pkg/sirenia/state\"\n+\t\"github.com/flynn/flynn/pkg/sirenia/xlog\"\n+)\n+\n+const (\n+\tDefaultPort        = \"3306\"\n+\tDefaultBinDir      = \"/usr/bin\"\n+\tDefaultSbinDir     = \"/usr/sbin\"\n+\tDefaultDataDir     = \"/data\"\n+\tDefaultPassword    = \"\"\n+\tDefaultOpTimeout   = 5 * time.Minute\n+\tDefaultReplTimeout = 1 * time.Minute\n+\n+\tBinName    = \"mysqld\"\n+\tConfigName = \"my.cnf\"\n+\n+\tcheckInterval = 1000 * time.Millisecond\n+)\n+\n+var (\n+\t// ErrRunning is returned when starting an already running process.\n+\tErrRunning = errors.New(\"process already running\")\n+\n+\t// ErrStopped is returned when stopping an already stopped process.\n+\tErrStopped = errors.New(\"process already stopped\")\n+\n+\tErrNoReplicationStatus = errors.New(\"no replication status\")\n+)\n+\n+// Process represents a MariaDB process.\n+type Process struct {\n+\tmtx sync.Mutex\n+\n+\tevents chan state.DatabaseEvent\n+\n+\t// Replication configuration\n+\tconfigValue   atomic.Value // *Config\n+\tconfigApplied bool\n+\n+\trunningValue          atomic.Value // bool\n+\tsyncedDownstreamValue atomic.Value // *discoverd.Instance\n+\n+\tID           string\n+\tSingleton    bool\n+\tPort         string\n+\tBinDir       string\n+\tSbinDir      string\n+\tDataDir      string\n+\tPassword     string\n+\tServerID     int\n+\tOpTimeout    time.Duration\n+\tReplTimeout  time.Duration\n+\tWaitUpstream bool\n+\n+\tLogger log15.Logger\n+\n+\t// cmd is the running system command.\n+\tcmd *Cmd\n+\n+\t// cancelSyncWait cancels the goroutine that is waiting for\n+\t// the downstream to catch up, if running.\n+\tcancelSyncWait func()\n+}\n+\n+// NewProcess returns a new instance of Process.\n+func NewProcess() *Process {\n+\tp := &Process{\n+\t\tPort:        DefaultPort,\n+\t\tBinDir:      DefaultBinDir,\n+\t\tSbinDir:     DefaultSbinDir,\n+\t\tDataDir:     DefaultDataDir,\n+\t\tPassword:    DefaultPassword,\n+\t\tOpTimeout:   DefaultOpTimeout,\n+\t\tReplTimeout: DefaultReplTimeout,\n+\t\tLogger:      log15.New(\"app\", \"mariadb\"),\n+\n+\t\tevents:         make(chan state.DatabaseEvent, 1),\n+\t\tcancelSyncWait: func() {},\n+\t}\n+\tp.runningValue.Store(false)\n+\tp.configValue.Store((*state.Config)(nil))\n+\treturn p\n+}\n+\n+func (p *Process) running() bool         { return p.runningValue.Load().(bool) }\n+func (p *Process) config() *state.Config { return p.configValue.Load().(*state.Config) }\n+\n+func (p *Process) syncedDownstream() *discoverd.Instance {\n+\treturn p.syncedDownstreamValue.Load().(*discoverd.Instance)\n+}\n+\n+func (p *Process) ConfigPath() string { return filepath.Join(p.DataDir, \"my.cnf\") }\n+\n+func (p *Process) Reconfigure(config *state.Config) error {\n+\tp.mtx.Lock()\n+\tdefer p.mtx.Unlock()\n+\n+\tswitch config.Role {\n+\tcase state.RolePrimary:\n+\t\tif !p.Singleton && config.Downstream == nil {\n+\t\t\treturn errors.New(\"missing downstream peer\")\n+\t\t}\n+\tcase state.RoleSync, state.RoleAsync:\n+\t\tif config.Upstream == nil {\n+\t\t\treturn fmt.Errorf(\"missing upstream peer\")\n+\t\t}\n+\tcase state.RoleNone:\n+\tdefault:\n+\t\treturn fmt.Errorf(\"unknown role %v\", config.Role)\n+\t}\n+\n+\tif !p.running() {\n+\t\tp.configValue.Store(config)\n+\t\tp.configApplied = false\n+\t\treturn nil\n+\t}\n+\n+\treturn p.reconfigure(config)\n+}\n+\n+func (p *Process) Start() error {\n+\tp.mtx.Lock()\n+\tdefer p.mtx.Unlock()\n+\n+\tif p.running() {\n+\t\treturn errors.New(\"process already running\")\n+\t}\n+\tif p.config() == nil {\n+\t\treturn errors.New(\"unconfigured process\")\n+\t}\n+\tif p.config().Role == state.RoleNone {\n+\t\treturn errors.New(\"start attempted with role 'none'\")\n+\t}\n+\n+\treturn p.reconfigure(nil)\n+}\n+\n+func (p *Process) Stop() error {\n+\tp.mtx.Lock()\n+\tdefer p.mtx.Unlock()\n+\n+\tif !p.running() {\n+\t\treturn errors.New(\"process already stopped\")\n+\t}\n+\treturn p.stop()\n+}\n+\n+func (p *Process) Ready() <-chan state.DatabaseEvent {\n+\treturn p.events\n+}\n+\n+func (p *Process) XLog() xlog.XLog {\n+\treturn MDBXLog{}\n+}\n+\n+func (p *Process) reconfigure(config *state.Config) error {\n+\tlogger := p.Logger.New(\"fn\", \"reconfigure\")\n+\n+\tif err := func() error {\n+\t\tif config != nil && config.Role == state.RoleNone {\n+\t\t\tlogger.Info(\"nothing to do\", \"reason\", \"null role\")\n+\t\t\treturn nil\n+\t\t}\n+\n+\t\t// If we've already applied the same config, we don't need to do anything\n+\t\tif p.configApplied && config != nil && p.config() != nil && config.Equal(p.config()) {\n+\t\t\tlogger.Info(\"nothing to do\", \"reason\", \"config already applied\")\n+\t\t\treturn nil\n+\t\t}\n+\n+\t\t// If we're already running and it's just a change from async to sync with the same node, we don't need to restart\n+\t\tif p.configApplied && p.running() && p.config() != nil && config != nil &&\n+\t\t\tp.config().Role == state.RoleAsync && config.Role == state.RoleSync && config.Upstream.Meta[\"MYSQL_ID\"] == p.config().Upstream.Meta[\"MYSQL_ID\"] {\n+\t\t\tlogger.Info(\"nothing to do\", \"reason\", \"becoming sync with same upstream\")\n+\t\t\treturn nil\n+\t\t}\n+\n+\t\t// Make sure that we don't keep waiting for replication sync while reconfiguring\n+\t\tp.cancelSyncWait()\n+\t\tp.syncedDownstreamValue.Store((*discoverd.Instance)(nil))\n+\n+\t\t// If we're already running and this is only a downstream change, just wait for the new downstream to catch up\n+\t\tif p.running() && p.config().IsNewDownstream(config) {\n+\t\t\tlogger.Info(\"downstream changed\", \"to\", config.Downstream.Addr)\n+\t\t\tp.waitForSync(config.Downstream, false)\n+\t\t\treturn nil\n+\t\t}\n+\n+\t\tif config == nil {\n+\t\t\tconfig = p.config()\n+\t\t}\n+\n+\t\tif config.Role == state.RolePrimary {\n+\t\t\treturn p.assumePrimary(config.Downstream)\n+\t\t}\n+\n+\t\treturn p.assumeStandby(config.Upstream, config.Downstream)\n+\t}(); err != nil {\n+\t\treturn err\n+\t}\n+\n+\t// Apply configuration.\n+\tp.configValue.Store(config)\n+\tp.configApplied = true\n+\n+\treturn nil\n+}\n+\n+func (p *Process) assumePrimary(downstream *discoverd.Instance) (err error) {\n+\tlogger := p.Logger.New(\"fn\", \"assumePrimary\")\n+\tif downstream != nil {\n+\t\tlogger = logger.New(\"downstream\", downstream.Addr)\n+\t}\n+\n+\tlogger.Info(\"starting as primary\")\n+\n+\t// Assert that the process is not running. This should not occur.\n+\tif p.running() {\n+\t\tpanic(fmt.Sprintf(\"unexpected state running role=%s\", p.config().Role))\n+\t}\n+\n+\tif err := p.writeConfig(configData{ReadOnly: downstream != nil}); err != nil {\n+\t\tlogger.Error(\"error writing config\", \"path\", p.ConfigPath(), \"err\", err)\n+\t\treturn err\n+\t}\n+\n+\tif err := p.installDB(); err != nil {\n+\t\treturn err\n+\t}\n+\n+\tif err := p.start(); err != nil {\n+\t\treturn err\n+\t}\n+\n+\tif err := p.initPrimaryDB(); err != nil {\n+\t\tif e := p.stop(); err != nil {\n+\t\t\tlogger.Debug(\"ignoring error stopping process\", \"err\", e)\n+\t\t}\n+\t\treturn err\n+\t}\n+\n+\tif downstream != nil {\n+\t\tp.waitForSync(downstream, true)\n+\t}\n+\n+\treturn nil\n+}\n+\n+// Backup returns a reader for streaming a backup in xbstream format.\n+func (p *Process) Backup() (io.ReadCloser, error) {\n+\tcmd := exec.Command(\n+\t\tfilepath.Join(p.BinDir, \"innobackupex\"),\n+\t\t\"--defaults-file=\"+p.ConfigPath(),\n+\t\t\"--host=127.0.0.1\",\n+\t\t\"--port=\"+p.Port,\n+\t\t\"--user=flynn\",\n+\t\t\"--password=\"+p.Password,\n+\t\t\"--socket=\",\n+\t\t\"--stream=xbstream\",\n+\t\t\".\",\n+\t)\n+\tstdout, err := cmd.StdoutPipe()\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tif err := cmd.Start(); err != nil {\n+\t\tstdout.Close()\n+\t\treturn nil, err\n+\t}\n+\treturn stdout, nil\n+}\n+\n+// Restore restores the database from an xbstream backup.\n+func (p *Process) Restore(r io.ReadCloser) error {\n+\tif err := p.writeConfig(configData{}); err != nil {\n+\t\treturn err\n+\t}\n+\tif err := p.unpackXbstream(r); err != nil {\n+\t\treturn err\n+\t}\n+\tif err := p.restoreApplyLog(); err != nil {\n+\t\treturn err\n+\t}\n+\treturn nil\n+}\n+\n+func (p *Process) unpackXbstream(r io.ReadCloser) error {\n+\tdefer r.Close()\n+\n+\tcmd := exec.Command(filepath.Join(p.BinDir, \"xbstream\"), \"-x\", \"--directory=\"+p.DataDir)\n+\tcmd.Stdin = r\n+\n+\tif buf, err := cmd.CombinedOutput(); err != nil {\n+\t\tp.Logger.Error(\"xbstream failed\", \"err\", err, \"output\", string(buf))\n+\t\treturn err\n+\t}\n+\treturn nil\n+}\n+\n+func (p *Process) restoreApplyLog() error {\n+\tcmd := exec.Command(\n+\t\tfilepath.Join(p.BinDir, \"innobackupex\"),\n+\t\t\"--defaults-file=\"+p.ConfigPath(),\n+\t\t\"--apply-log\",\n+\t\tp.DataDir,\n+\t)\n+\tif buf, err := cmd.CombinedOutput(); err != nil {\n+\t\tp.Logger.Error(\"innobackupex apply-log failed\", \"err\", err, \"output\", string(buf))\n+\t\treturn err\n+\t}\n+\treturn nil\n+}\n+\n+func (p *Process) assumeStandby(upstream, downstream *discoverd.Instance) error {\n+\tlogger := p.Logger.New(\"fn\", \"assumeStandby\", \"upstream\", upstream.Addr)\n+\tlogger.Info(\"starting up as standby\")\n+\n+\tif err := p.writeConfig(configData{ReadOnly: true}); err != nil {\n+\t\tlogger.Error(\"error writing config\", \"path\", p.ConfigPath(), \"err\", err)\n+\t\treturn err\n+\t}\n+\n+\tif p.running() {\n+\t\tif err := p.stop(); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t} else {\n+\t\tif p.WaitUpstream {\n+\t\t\tif err := p.waitForUpstream(upstream); err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\t\t}\n+\n+\t\tif err := func() error {\n+\t\t\t// TODO(benbjohnson): Lock table during backup: \"FLUSH TABLES WITH READ LOCK\"\n+\n+\t\t\tlogger.Info(\"retrieving backup\")\n+\t\t\tresp, err := http.Get(fmt.Sprintf(\"http://%s/backup\", upstream.Meta[\"HTTP_ADDR\"]))\n+\t\t\tif err != nil {\n+\t\t\t\tlogger.Error(\"error connecting to upstream for backup\", \"err\", err)\n+\t\t\t\treturn err\n+\t\t\t} else if resp.StatusCode != http.StatusOK {\n+\t\t\t\tresp.Body.Close()\n+\t\t\t\tlogger.Error(\"error code returned from backup\", \"status_code\", resp.StatusCode)\n+\t\t\t\treturn err\n+\t\t\t}\n+\n+\t\t\tlogger.Info(\"restoring backup\")\n+\t\t\tif err := p.Restore(resp.Body); err != nil {\n+\t\t\t\tlogger.Error(\"error restoring backup\", \"err\", err)\n+\t\t\t\treturn err\n+\t\t\t}\n+\n+\t\t\t// TODO(benbjohnson): Unlock tables after backup and log pos are retrieved.\n+\n+\t\t\treturn nil\n+\t\t}(); err != nil {\n+\t\t\tif files, err := ioutil.ReadDir(\"/data\"); err == nil {\n+\t\t\t\tfor _, file := range files {\n+\t\t\t\t\tos.RemoveAll(filepath.Join(\"/data\", file.Name()))\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\treturn err\n+\t\t}\n+\t}\n+\n+\tif err := p.start(); err != nil {\n+\t\treturn err\n+\t}\n+\n+\tif err := func() error {\n+\t\tlogger.Info(\"retrieving upstream log coordinates\")\n+\t\tms, err := p.masterStatus(upstream.Addr)\n+\t\tif err != nil {\n+\t\t\tlogger.Error(\"error reading upstream log coordinates\", \"err\", err)\n+\t\t\treturn err\n+\t\t}\n+\n+\t\t// Connect to local server and set up slave replication.\n+\t\tdsn := p.DSN(\"127.0.0.1:\"+p.Port, \"\")\n+\t\tdsn.User = \"root\"\n+\t\tdsn.Password = \"\"\n+\n+\t\tdb, err := sql.Open(\"mysql\", dsn.String())\n+\t\tif err != nil {\n+\t\t\tlogger.Error(\"error acquiring connection\", \"err\", err)\n+\t\t\treturn err\n+\t\t}\n+\t\tdefer db.Close()\n+\n+\t\thost, port, _ := net.SplitHostPort(upstream.Addr)\n+\t\tlogger.Info(\"changing master\", \"host\", host, \"port\", port)\n+\t\tif _, err := db.Exec(fmt.Sprintf(\n+\t\t\t`CHANGE MASTER TO MASTER_HOST='%s', MASTER_PORT=%s, MASTER_USER='flynn', MASTER_PASSWORD='%s', MASTER_LOG_FILE='%s', MASTER_LOG_POS=%d, MASTER_CONNECT_RETRY=10;`,\n+\t\t\thost,\n+\t\t\tport,\n+\t\t\tp.Password,\n+\t\t\tms.File,\n+\t\t\tms.Position,\n+\t\t)); err != nil {\n+\t\t\tlogger.Error(\"error changing master\", \"host\", host, \"port\", port, \"file\", ms.File, \"position\", ms.Position, \"err\", err)\n+\t\t\treturn err\n+\t\t}\n+\n+\t\t// Start slave.\n+\t\tlogger.Info(\"starting slave\")\n+\t\tif _, err := db.Exec(`START SLAVE`); err != nil {\n+\t\t\treturn err\n+\n+\t\t}\n+\n+\t\t// Install semi-sync slave plugin.\n+\t\tif _, err := db.Exec(`INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'`); err != nil {\n+\t\t\tlogger.Error(\"error installing rpl_semi_sync_slave\", \"err\", err)\n+\t\t\treturn err\n+\t\t}\n+\t\tif _, err := db.Exec(`STOP SLAVE IO_THREAD`); err != nil {\n+\t\t\tlogger.Error(\"error stopping slave io thread\", \"err\", err)\n+\t\t\treturn err\n+\t\t}\n+\t\tif _, err := db.Exec(`START SLAVE IO_THREAD`); err != nil {\n+\t\t\tlogger.Error(\"error starting slave io thread\", \"err\", err)\n+\t\t\treturn err\n+\t\t}\n+\n+\t\t// Enable semi-synchronous on slave.\n+\t\tif _, err := db.Exec(`SET GLOBAL rpl_semi_sync_slave_enabled = ON`); err != nil {\n+\t\t\treturn err\n+\t\t}\n+\n+\t\treturn nil\n+\t}(); err != nil {\n+\t\treturn err\n+\t}\n+\n+\tif downstream != nil {\n+\t\tp.waitForSync(downstream, false)\n+\t}\n+\n+\treturn nil\n+}\n+\n+// initPrimaryDB initializes the local database with the correct users and plugins.\n+func (p *Process) initPrimaryDB() error {\n+\tlogger := p.Logger.New(\"fn\", \"initPrimaryDB\")\n+\tlogger.Info(\"initializing primary database\")\n+\n+\tdsn := p.DSN(\"127.0.0.1:\"+p.Port, \"\")\n+\tdsn.User = \"root\"\n+\tdsn.Password = \"\"\n+\n+\tdb, err := sql.Open(\"mysql\", dsn.String())\n+\tif err != nil {\n+\t\tlogger.Error(\"error acquiring connection\", \"err\", err)\n+\t\treturn err\n+\t}\n+\tdefer db.Close()\n+\n+\tif _, err := db.Exec(fmt.Sprintf(`CREATE USER 'flynn'@'%%' IDENTIFIED BY '%s'`, p.Password)); err != nil {\n+\t\tlogger.Error(\"error creating database user\", \"err\", err)\n+\t\treturn err\n+\t}\n+\tif _, err := db.Exec(`GRANT ALL PRIVILEGES ON *.* TO 'flynn'@'%'`); err != nil {\n+\t\tlogger.Error(\"error granting privileges\", \"err\", err)\n+\t\treturn err\n+\t}\n+\tif _, err := db.Exec(`GRANT REPLICATION SLAVE ON *.* TO 'flynn'@'%'`); err != nil {\n+\t\tlogger.Error(\"error granting replication privileges\", \"err\", err)\n+\t\treturn err\n+\t}\n+\tif _, err := db.Exec(`INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'`); err != nil {\n+\t\tlogger.Error(\"error installing rpl_semi_sync_master\", \"err\", err)\n+\t\treturn err\n+\t}\n+\tif _, err := db.Exec(`FLUSH PRIVILEGES`); err != nil {\n+\t\tlogger.Error(\"error flushing privileges\", \"err\", err)\n+\t\treturn err\n+\t}\n+\n+\t// Enable semi-sync replication on the master.\n+\tif _, err := db.Exec(`SET GLOBAL rpl_semi_sync_master_enabled = ON`); err != nil {","path":"appliance/mariadb/process.go","position":507,"original_position":507,"commit_id":"35096f8458aefa12ebe369f85e72e97c977bc487","original_commit_id":"35096f8458aefa12ebe369f85e72e97c977bc487","user":{"login":"josephglanville","id":605591,"avatar_url":"https://avatars.githubusercontent.com/u/605591?v=3","gravatar_id":"","url":"https://api.github.com/users/josephglanville","html_url":"https://github.com/josephglanville","followers_url":"https://api.github.com/users/josephglanville/followers","following_url":"https://api.github.com/users/josephglanville/following{/other_user}","gists_url":"https://api.github.com/users/josephglanville/gists{/gist_id}","starred_url":"https://api.github.com/users/josephglanville/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/josephglanville/subscriptions","organizations_url":"https://api.github.com/users/josephglanville/orgs","repos_url":"https://api.github.com/users/josephglanville/repos","events_url":"https://api.github.com/users/josephglanville/events{/privacy}","received_events_url":"https://api.github.com/users/josephglanville/received_events","type":"User","site_admin":false},"body":"We are also going to need to set a few other settings, may actually be better to set them in the configuration file though.\r\n\r\nSpecifically `rpl_semi_sync_master_wait_no_slave` is required to ensure the timeout will be observed even when the sync isn't connected. Otherwise the primary will automatically switch to async replication and can commit writes that are not replicated anywhere. The timeout itself too needs to be raised to a sufficiently high value that it will never be reached too, which is `rpl_semi_sync_master_timeout`.\r\nWe should also ensure that `rpl_semi_sync_master_wait_point` is set to `AFTER_SYNC` which is the safest/most-consistent.","created_at":"2016-02-10T18:13:23Z","updated_at":"2016-02-10T18:13:23Z","html_url":"https://github.com/flynn/flynn/pull/2433#discussion_r52498957","pull_request_url":"https://api.github.com/repos/flynn/flynn/pulls/2433","_links":{"self":{"href":"https://api.github.com/repos/flynn/flynn/pulls/comments/52498957"},"html":{"href":"https://github.com/flynn/flynn/pull/2433#discussion_r52498957"},"pull_request":{"href":"https://api.github.com/repos/flynn/flynn/pulls/2433"}}},"pull_request":{"url":"https://api.github.com/repos/flynn/flynn/pulls/2433","id":58707325,"html_url":"https://github.com/flynn/flynn/pull/2433","diff_url":"https://github.com/flynn/flynn/pull/2433.diff","patch_url":"https://github.com/flynn/flynn/pull/2433.patch","issue_url":"https://api.github.com/repos/flynn/flynn/issues/2433","number":2433,"state":"open","locked":false,"title":"Mariadb integration","user":{"login":"josephglanville","id":605591,"avatar_url":"https://avatars.githubusercontent.com/u/605591?v=3","gravatar_id":"","url":"https://api.github.com/users/josephglanville","html_url":"https://github.com/josephglanville","followers_url":"https://api.github.com/users/josephglanville/followers","following_url":"https://api.github.com/users/josephglanville/following{/other_user}","gists_url":"https://api.github.com/users/josephglanville/gists{/gist_id}","starred_url":"https://api.github.com/users/josephglanville/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/josephglanville/subscriptions","organizations_url":"https://api.github.com/users/josephglanville/orgs","repos_url":"https://api.github.com/users/josephglanville/repos","events_url":"https://api.github.com/users/josephglanville/events{/privacy}","received_events_url":"https://api.github.com/users/josephglanville/received_events","type":"User","site_admin":false},"body":"Integration branch for MariaDB appliance.","created_at":"2016-02-09T05:41:09Z","updated_at":"2016-02-10T18:13:23Z","closed_at":null,"merged_at":null,"merge_commit_sha":"ef3208f80b0a5b1819b1e0573de78ec6fd98cb12","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/flynn/flynn/pulls/2433/commits","review_comments_url":"https://api.github.com/repos/flynn/flynn/pulls/2433/comments","review_comment_url":"https://api.github.com/repos/flynn/flynn/pulls/comments{/number}","comments_url":"https://api.github.com/repos/flynn/flynn/issues/2433/comments","statuses_url":"https://api.github.com/repos/flynn/flynn/statuses/35096f8458aefa12ebe369f85e72e97c977bc487","head":{"label":"flynn:mariadb-integration","ref":"mariadb-integration","sha":"35096f8458aefa12ebe369f85e72e97c977bc487","user":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"repo":{"id":11290232,"name":"flynn","full_name":"flynn/flynn","owner":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/flynn/flynn","description":"A next generation open source platform as a service (PaaS)","fork":false,"url":"https://api.github.com/repos/flynn/flynn","forks_url":"https://api.github.com/repos/flynn/flynn/forks","keys_url":"https://api.github.com/repos/flynn/flynn/keys{/key_id}","collaborators_url":"https://api.github.com/repos/flynn/flynn/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/flynn/flynn/teams","hooks_url":"https://api.github.com/repos/flynn/flynn/hooks","issue_events_url":"https://api.github.com/repos/flynn/flynn/issues/events{/number}","events_url":"https://api.github.com/repos/flynn/flynn/events","assignees_url":"https://api.github.com/repos/flynn/flynn/assignees{/user}","branches_url":"https://api.github.com/repos/flynn/flynn/branches{/branch}","tags_url":"https://api.github.com/repos/flynn/flynn/tags","blobs_url":"https://api.github.com/repos/flynn/flynn/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/flynn/flynn/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/flynn/flynn/git/refs{/sha}","trees_url":"https://api.github.com/repos/flynn/flynn/git/trees{/sha}","statuses_url":"https://api.github.com/repos/flynn/flynn/statuses/{sha}","languages_url":"https://api.github.com/repos/flynn/flynn/languages","stargazers_url":"https://api.github.com/repos/flynn/flynn/stargazers","contributors_url":"https://api.github.com/repos/flynn/flynn/contributors","subscribers_url":"https://api.github.com/repos/flynn/flynn/subscribers","subscription_url":"https://api.github.com/repos/flynn/flynn/subscription","commits_url":"https://api.github.com/repos/flynn/flynn/commits{/sha}","git_commits_url":"https://api.github.com/repos/flynn/flynn/git/commits{/sha}","comments_url":"https://api.github.com/repos/flynn/flynn/comments{/number}","issue_comment_url":"https://api.github.com/repos/flynn/flynn/issues/comments{/number}","contents_url":"https://api.github.com/repos/flynn/flynn/contents/{+path}","compare_url":"https://api.github.com/repos/flynn/flynn/compare/{base}...{head}","merges_url":"https://api.github.com/repos/flynn/flynn/merges","archive_url":"https://api.github.com/repos/flynn/flynn/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/flynn/flynn/downloads","issues_url":"https://api.github.com/repos/flynn/flynn/issues{/number}","pulls_url":"https://api.github.com/repos/flynn/flynn/pulls{/number}","milestones_url":"https://api.github.com/repos/flynn/flynn/milestones{/number}","notifications_url":"https://api.github.com/repos/flynn/flynn/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/flynn/flynn/labels{/name}","releases_url":"https://api.github.com/repos/flynn/flynn/releases{/id}","deployments_url":"https://api.github.com/repos/flynn/flynn/deployments","created_at":"2013-07-09T18:22:06Z","updated_at":"2016-02-10T17:10:22Z","pushed_at":"2016-02-10T13:51:39Z","git_url":"git://github.com/flynn/flynn.git","ssh_url":"git@github.com:flynn/flynn.git","clone_url":"https://github.com/flynn/flynn.git","svn_url":"https://github.com/flynn/flynn","homepage":"https://flynn.io","size":24237,"stargazers_count":4244,"watchers_count":4244,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":311,"mirror_url":null,"open_issues_count":282,"forks":311,"open_issues":282,"watchers":4244,"default_branch":"master"}},"base":{"label":"flynn:master","ref":"master","sha":"7454702267720f394c10d61d6a7c45dde9fd13d7","user":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"repo":{"id":11290232,"name":"flynn","full_name":"flynn/flynn","owner":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/flynn/flynn","description":"A next generation open source platform as a service (PaaS)","fork":false,"url":"https://api.github.com/repos/flynn/flynn","forks_url":"https://api.github.com/repos/flynn/flynn/forks","keys_url":"https://api.github.com/repos/flynn/flynn/keys{/key_id}","collaborators_url":"https://api.github.com/repos/flynn/flynn/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/flynn/flynn/teams","hooks_url":"https://api.github.com/repos/flynn/flynn/hooks","issue_events_url":"https://api.github.com/repos/flynn/flynn/issues/events{/number}","events_url":"https://api.github.com/repos/flynn/flynn/events","assignees_url":"https://api.github.com/repos/flynn/flynn/assignees{/user}","branches_url":"https://api.github.com/repos/flynn/flynn/branches{/branch}","tags_url":"https://api.github.com/repos/flynn/flynn/tags","blobs_url":"https://api.github.com/repos/flynn/flynn/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/flynn/flynn/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/flynn/flynn/git/refs{/sha}","trees_url":"https://api.github.com/repos/flynn/flynn/git/trees{/sha}","statuses_url":"https://api.github.com/repos/flynn/flynn/statuses/{sha}","languages_url":"https://api.github.com/repos/flynn/flynn/languages","stargazers_url":"https://api.github.com/repos/flynn/flynn/stargazers","contributors_url":"https://api.github.com/repos/flynn/flynn/contributors","subscribers_url":"https://api.github.com/repos/flynn/flynn/subscribers","subscription_url":"https://api.github.com/repos/flynn/flynn/subscription","commits_url":"https://api.github.com/repos/flynn/flynn/commits{/sha}","git_commits_url":"https://api.github.com/repos/flynn/flynn/git/commits{/sha}","comments_url":"https://api.github.com/repos/flynn/flynn/comments{/number}","issue_comment_url":"https://api.github.com/repos/flynn/flynn/issues/comments{/number}","contents_url":"https://api.github.com/repos/flynn/flynn/contents/{+path}","compare_url":"https://api.github.com/repos/flynn/flynn/compare/{base}...{head}","merges_url":"https://api.github.com/repos/flynn/flynn/merges","archive_url":"https://api.github.com/repos/flynn/flynn/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/flynn/flynn/downloads","issues_url":"https://api.github.com/repos/flynn/flynn/issues{/number}","pulls_url":"https://api.github.com/repos/flynn/flynn/pulls{/number}","milestones_url":"https://api.github.com/repos/flynn/flynn/milestones{/number}","notifications_url":"https://api.github.com/repos/flynn/flynn/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/flynn/flynn/labels{/name}","releases_url":"https://api.github.com/repos/flynn/flynn/releases{/id}","deployments_url":"https://api.github.com/repos/flynn/flynn/deployments","created_at":"2013-07-09T18:22:06Z","updated_at":"2016-02-10T17:10:22Z","pushed_at":"2016-02-10T13:51:39Z","git_url":"git://github.com/flynn/flynn.git","ssh_url":"git@github.com:flynn/flynn.git","clone_url":"https://github.com/flynn/flynn.git","svn_url":"https://github.com/flynn/flynn","homepage":"https://flynn.io","size":24237,"stargazers_count":4244,"watchers_count":4244,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":311,"mirror_url":null,"open_issues_count":282,"forks":311,"open_issues":282,"watchers":4244,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/flynn/flynn/pulls/2433"},"html":{"href":"https://github.com/flynn/flynn/pull/2433"},"issue":{"href":"https://api.github.com/repos/flynn/flynn/issues/2433"},"comments":{"href":"https://api.github.com/repos/flynn/flynn/issues/2433/comments"},"review_comments":{"href":"https://api.github.com/repos/flynn/flynn/pulls/2433/comments"},"review_comment":{"href":"https://api.github.com/repos/flynn/flynn/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/flynn/flynn/pulls/2433/commits"},"statuses":{"href":"https://api.github.com/repos/flynn/flynn/statuses/35096f8458aefa12ebe369f85e72e97c977bc487"}}}}