diff --git a/Makefile b/Makefile index 7f4d1c19..744edb65 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,7 @@ testdaemon: rm -f ./inertia-daemon-image docker build -t ubclaunchpad/inertia:test . docker save -o ./inertia-daemon-image ubclaunchpad/inertia:test + docker rmi ubclaunchpad/inertia:test chmod 400 ./test/keys/id_rsa scp -i ./test/keys/id_rsa \ -o StrictHostKeyChecking=no \ diff --git a/client/bootstrap.go b/client/bootstrap.go index 5e917026..cfe14ab8 100644 --- a/client/bootstrap.go +++ b/client/bootstrap.go @@ -87,12 +87,12 @@ func clientBootstrapDaemonDownSh() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "client/bootstrap/daemon-down.sh", size: 233, mode: os.FileMode(420), modTime: time.Unix(1517083020, 0)} + info := bindataFileInfo{name: "client/bootstrap/daemon-down.sh", size: 233, mode: os.FileMode(420), modTime: time.Unix(1522812136, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _clientBootstrapDaemonUpSh = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x6c\x54\x51\x6f\xdb\x38\x0c\x7e\xd7\xaf\xe0\x9c\x14\x7d\x99\xec\x76\xc3\x01\xbb\x0e\x7e\xc8\xad\xc6\x1a\xac\x4d\x8a\xa4\x87\xc3\xa1\x57\x64\x8a\xcd\xd4\x5a\x6c\x49\x27\xd2\xc9\xd6\x5f\x3f\xc8\x4e\xe2\xa4\xe8\x53\x62\x89\xfc\xf8\xf1\xe3\x27\x0e\xe0\x2f\x45\x3a\x07\xca\xbd\x76\x0c\x2b\xeb\x61\xe9\xb5\x79\xd6\xe6\x19\x1a\x07\x5c\x22\x14\x0a\x6b\x6b\x62\x21\x08\x19\x24\x0a\x71\x3d\xca\xee\xa6\x93\xc5\x2c\xbb\xcd\x46\xf3\x2c\x3d\x7b\xbc\x7c\xa2\xfd\xe1\xfd\x74\xf6\x90\x9e\x3d\x7e\x78\x22\x71\x33\x9d\x3f\x2c\x46\xd7\xd7\xb3\x6c\x3e\x4f\xcf\x1e\x3f\x3e\xd1\x21\x75\x32\xba\xcb\x52\x6d\xd0\xb3\x56\xb2\xc3\x17\xe3\xbb\xd1\xd7\x2c\x6d\x96\x79\xa5\x1a\x93\x97\x4e\x15\xc9\x2e\xe2\x6a\x78\x5a\x51\x7c\x99\x4e\x1e\x46\xe3\x49\x36\xeb\xca\x7d\xba\xf8\x74\x29\xc4\x00\xe6\xc8\x81\x73\xa1\x3d\xe6\x6c\xbd\x46\x12\xf5\xba\xd0\x1e\xa4\x83\xe1\xcd\xf4\x2e\x4b\x9c\xb7\x3f\x30\xe7\xd7\xc7\x44\x55\xc8\xff\x52\x62\xbe\x06\xbd\x02\x55\x79\x54\xc5\x2f\xf0\x8d\x31\x41\x08\x65\x0a\x60\xb5\x46\x28\xec\xd6\x00\xfe\xd4\xc4\xe1\x78\x2f\xcc\xe8\x76\x96\x8d\xae\xff\x5d\xcc\xfe\x9e\x4c\xc6\x93\xaf\xe9\x77\x6a\x0a\x0b\x85\xcd\xd7\xe8\xc1\x11\xc8\xff\x41\xca\x95\xae\x18\x3d\x44\x46\xd5\x98\x0e\x8f\x74\x88\xbe\x0b\xbd\x82\x47\x78\x07\xf2\x05\xa2\xe1\x2b\xb0\x08\x9e\x3e\x87\x29\x18\x01\x00\x80\x79\x69\x21\xfa\xa6\xab\x2a\xd4\x3f\x10\xc9\xad\x61\x15\xc4\x8a\xe3\x38\x6a\x03\x8f\x19\xf8\x1a\xe4\x0a\x5e\x03\x8b\x95\xfe\x1c\x9a\xbe\xf7\xe8\x94\x47\x50\xce\x79\xeb\xbc\x56\xbc\x9f\x38\xe8\x5a\x3d\x63\xdc\xb1\x8b\x5e\xcd\x20\x82\x77\x29\x44\x8c\xc4\xa7\x0c\x07\x70\xdf\x54\x55\xeb\x9b\xdd\xf4\x0e\x32\xf5\x0d\x84\x90\xc0\x7b\x7c\x1a\xf1\x16\x79\x17\xd0\x86\xad\x37\x04\x56\x84\x47\x28\xb7\xad\x4f\x4e\x84\x38\x05\xdc\x35\xf0\x16\x6c\x65\x55\x01\x52\x43\xd2\x45\xca\x36\x52\xac\x74\x50\x64\xd6\x98\x5e\x51\xd8\x6a\x2e\x41\xe5\x39\x12\x01\xdb\xb6\xb1\xd2\x12\xef\x81\x28\xfc\x70\xeb\x10\x8f\x15\x6e\x94\xe1\x63\x03\x82\x14\x03\xe0\x52\x13\x68\x02\x83\x01\x45\xf9\x5f\xb0\xc4\x5c\x35\x84\xb0\x45\xd8\x86\x8c\xfe\x99\x85\x1a\x4b\x04\xb5\xac\x10\x88\x95\x67\x31\x68\xc1\x89\xad\xeb\x59\x11\x84\xc8\x1d\x95\x18\xc6\x7c\x4e\xa0\x2a\xb2\x6d\x84\xb7\x1b\xf4\xa4\x55\xf5\x5e\x0c\xa0\x64\x76\x74\x95\x24\xdb\xed\x36\xae\x36\x65\xac\x6d\xe2\x2c\x31\x25\x85\x35\x2c\xf1\xa7\xb3\x84\x92\x4b\x94\x5d\x3f\xb2\xeb\x47\x1a\xcb\x12\x37\x68\x24\x5b\xa9\x64\xef\xaf\x92\xeb\x4a\x0c\x8e\x0a\x7a\xcc\x6d\x5d\xa3\x29\xb0\x38\x2e\xf7\xc3\x21\xab\x97\x17\x1b\x3f\x6b\x2e\x9b\x65\x28\xfb\xe1\xe2\xf2\x8f\xe4\xe2\xcf\xe4\xe2\x63\x52\xd8\xb6\x40\x43\x87\xb2\xda\xec\xff\xad\xac\x97\xb9\x4e\xc4\x00\x46\x04\x0a\x3c\x52\x53\xf1\xfb\x4e\xc3\x7e\x2a\xa5\x22\xf0\xd6\xf2\x7e\x32\x3b\x39\x3c\xd6\x96\x11\x36\x8e\x62\x71\xf2\x04\x1a\x03\xb2\x00\x29\x7d\x0d\xff\xb5\x66\x90\xae\xb7\x74\x58\x22\xd1\x55\x34\x3c\x5d\x2b\xd1\x3e\x72\x03\xc9\x46\xf9\xc4\x37\x26\xe9\xe0\xe2\x20\xd2\xd5\x5b\x87\x7d\x4a\xd4\x6e\x96\xe8\x2a\x51\xce\x25\xad\x61\x76\x57\x08\xe1\x22\xdd\xdd\xf7\xa7\xf3\xf9\xcd\xe2\xdb\x64\xfa\xcf\x64\x11\xf6\xe6\x3c\x3d\x3f\x64\x26\x31\x51\x99\xac\x8d\xdd\x9a\x45\xf8\xa6\xf3\x7d\x96\x0c\xcb\xa4\xef\xa3\xdd\x26\xbb\xbb\xa8\x7b\x33\x51\x4b\xa4\xdf\xc3\x91\xf8\x1d\x00\x00\xff\xff\x74\x47\xad\x86\xf1\x05\x00\x00") +var _clientBootstrapDaemonUpSh = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x6c\x54\x51\x6f\xdb\x38\x0c\x7e\xd7\xaf\xe0\x9c\x14\x7d\x99\xec\x76\xc3\x01\xbb\x0e\x7e\xc8\xad\xc6\x1a\xac\x4d\x8a\xa4\x87\xc3\xa1\x57\x64\x8a\xcd\xd4\x5a\x6c\x49\x13\xe9\x64\xeb\xaf\x3f\xc8\x4e\xe2\xa4\xe8\x53\x62\x89\xfc\xf8\xf1\xe3\x27\x0e\xe0\x2f\x45\x3a\x07\xca\xbd\x76\x0c\x2b\xeb\x61\xe9\xb5\x79\xd6\xe6\x19\x1a\x07\x5c\x22\x14\x0a\x6b\x6b\x62\x21\x08\x19\x24\x0a\x71\x3d\xca\xee\xa6\x93\xc5\x2c\xbb\xcd\x46\xf3\x2c\x3d\x7b\xbc\x7c\xa2\xfd\xe1\xfd\x74\xf6\x90\x9e\x3d\x7e\x78\x22\x71\x33\x9d\x3f\x2c\x46\xd7\xd7\xb3\x6c\x3e\x4f\xcf\x1e\x3f\x3e\xd1\x21\x75\x32\xba\xcb\x52\x6d\xd0\xb3\x56\xb2\xc3\x17\xe3\xbb\xd1\xd7\x2c\x6d\x96\x79\xa5\x1a\x93\x97\x4e\x15\xc9\x2e\xe2\x6a\x78\x5a\x51\x7c\x99\x4e\x1e\x46\xe3\x49\x36\xeb\xca\x7d\xba\xf8\x74\x29\xc4\x00\xe6\xc8\x81\x73\xa1\x3d\xe6\x6c\xbd\x46\x12\xf5\xba\xd0\x1e\xa4\x83\xe1\xcd\xf4\x2e\x4b\x9c\xb7\x3f\x30\xe7\xd7\xc7\x44\x55\xc8\xff\x52\x62\xbe\x06\xbd\x02\x55\x79\x54\xc5\x6f\xf0\x8d\x31\x41\x08\x65\x0a\x60\xb5\x46\x28\xec\xd6\x00\xfe\xd2\xc4\xe1\x78\x2f\xcc\xe8\x76\x96\x8d\xae\xff\x5d\xcc\xfe\x9e\x4c\xc6\x93\xaf\xe9\x77\x6a\x0a\x0b\x85\xcd\xd7\xe8\xc1\x11\xc8\x9f\x20\xe5\x4a\x57\x8c\x1e\x22\xa3\x6a\x4c\x87\x47\x3a\x44\xdf\x85\x5e\xc1\x23\xbc\x03\xf9\x02\xd1\xf0\x15\x58\x04\x4f\x9f\xc3\x14\x8c\x00\x00\xc0\xbc\xb4\x10\x7d\xd3\x55\x15\xea\x1f\x88\xe4\xd6\xb0\x0a\x62\xc5\x71\x1c\xb5\x81\xc7\x0c\x7c\x0d\x72\x05\xaf\x81\xc5\x4a\x7f\x0e\x4d\xdf\x7b\x74\xca\x23\x28\xe7\xbc\x75\x5e\x2b\xde\x4f\x1c\x74\xad\x9e\x31\xee\xd8\x45\xaf\x66\x10\xc1\xbb\x14\x22\x46\xe2\x53\x86\x03\xb8\x6f\xaa\xaa\xf5\xcd\x6e\x7a\x07\x99\xfa\x06\x42\x48\xe0\x3d\x3e\x8d\x78\x8b\xbc\x0b\x68\xc3\xd6\x1b\x02\x2b\xc2\x23\x94\x5b\xab\x8a\x13\x19\x4e\xe1\x76\xf4\xdf\x02\xad\xac\x2a\x40\xca\x9f\x8d\x0e\x8e\xd6\x90\x74\x19\xb2\xcd\x10\x2b\x1d\x74\x99\x35\xa6\xd7\x15\xb6\x9a\x4b\x50\x79\x8e\x44\xc0\xb6\x6d\xaf\xb4\xc4\x7b\x40\x0a\x3f\xdc\xfa\xc4\x63\x85\x1b\x65\xf8\xd8\x86\x20\xc5\x00\xb8\xd4\x04\x9a\xc0\x60\x40\x51\xfe\x37\x2c\x31\x57\x0d\x21\x6c\x11\xb6\x21\xa3\x7f\x6c\xa1\xc6\x12\x41\x2d\x2b\x04\x62\xe5\x59\x0c\x5a\x70\x62\xeb\x7a\x56\x04\x21\x72\x47\x25\x86\x31\x9f\x13\xa8\x8a\x6c\x1b\xe1\xed\x06\x3d\x69\x55\xbd\x17\x03\x28\x99\x1d\x5d\x25\xc9\x76\xbb\x8d\xab\x4d\x19\x6b\x9b\x38\x4b\x4c\x49\x61\x0d\x4b\xfc\xe5\x2c\xa1\xe4\x12\x65\xd7\x8f\xec\xfa\x91\xc6\xb2\xc4\x0d\x1a\xc9\x56\x2a\xd9\xbb\xac\xe4\xba\x12\x83\xa3\x82\x1e\x73\x5b\xd7\x68\x0a\x2c\x8e\xcb\xfd\x70\xc8\xea\xe5\xc5\xc6\xcf\x9a\xcb\x66\x19\xca\x7e\xb8\xb8\xfc\x23\xb9\xf8\x33\xb9\xf8\x98\x14\xb6\x2d\xd0\xd0\xa1\xac\x36\xfb\x7f\x2b\xeb\x65\xae\x13\x31\x80\x11\x81\x02\x8f\xd4\x54\xfc\xbe\xd3\xb0\x9f\x4a\xa9\x08\xbc\xb5\xbc\x9f\xcc\x4e\x0e\x8f\xb5\x65\x84\x8d\xa3\x58\x9c\x3c\x84\xc6\x80\x0c\x83\xf7\x35\xfc\xd7\x9a\x42\xba\xde\xd8\x61\x95\x44\x57\xd1\xf0\x74\xb9\x44\xfb\xc8\x0d\x24\x1b\xe5\x13\xdf\x98\xa4\x83\x8b\x83\x48\x57\x6f\x1d\xf6\x29\x51\xbb\x5f\xa2\xab\x44\x39\x97\xb4\x86\xd9\x5d\x21\x84\x8b\x74\x77\xdf\x9f\xce\xe7\x37\x8b\x6f\x93\xe9\x3f\x93\x45\xd8\x9e\xf3\xf4\xfc\x90\x99\xc4\x44\x65\xb2\x36\x76\x6b\x16\xe1\x9b\xce\xf7\x59\x32\xac\x94\xbe\x8f\x76\xa7\xec\xee\xa2\xee\xe5\x44\x2d\x91\x7e\x1b\x47\xe2\xff\x00\x00\x00\xff\xff\xf6\xc7\xff\x7f\xf7\x05\x00\x00") func clientBootstrapDaemonUpShBytes() ([]byte, error) { return bindataRead( @@ -107,7 +107,7 @@ func clientBootstrapDaemonUpSh() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "client/bootstrap/daemon-up.sh", size: 1521, mode: os.FileMode(493), modTime: time.Unix(1521770705, 0)} + info := bindataFileInfo{name: "client/bootstrap/daemon-up.sh", size: 1527, mode: os.FileMode(493), modTime: time.Unix(1522816187, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -127,7 +127,7 @@ func clientBootstrapDockerSh() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "client/bootstrap/docker.sh", size: 1045, mode: os.FileMode(493), modTime: time.Unix(1522898768, 0)} + info := bindataFileInfo{name: "client/bootstrap/docker.sh", size: 1045, mode: os.FileMode(493), modTime: time.Unix(1522901380, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -147,7 +147,7 @@ func clientBootstrapKeygenSh() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "client/bootstrap/keygen.sh", size: 589, mode: os.FileMode(493), modTime: time.Unix(1517083020, 0)} + info := bindataFileInfo{name: "client/bootstrap/keygen.sh", size: 589, mode: os.FileMode(493), modTime: time.Unix(1522017202, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -167,7 +167,7 @@ func clientBootstrapTokenSh() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "client/bootstrap/token.sh", size: 309, mode: os.FileMode(493), modTime: time.Unix(1521770705, 0)} + info := bindataFileInfo{name: "client/bootstrap/token.sh", size: 309, mode: os.FileMode(493), modTime: time.Unix(1522812136, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/client/bootstrap/daemon-up.sh b/client/bootstrap/daemon-up.sh index 3a44db9b..89856462 100755 --- a/client/bootstrap/daemon-up.sh +++ b/client/bootstrap/daemon-up.sh @@ -27,8 +27,8 @@ if [ "$DAEMON_RELEASE" != "test" ]; then echo "Pulling Inertia daemon..." sudo docker pull $IMAGE else - echo "Launching existing Inertia daemon image..." - sudo docker load -i /daemon-image + echo "Loading existing Inertia daemon image..." + sudo docker load --quiet -i /daemon-image fi # Run container with access to the host docker socket and relevant directories - diff --git a/client/config.go b/client/config.go index 910b03b5..6e7d0d76 100644 --- a/client/config.go +++ b/client/config.go @@ -20,10 +20,11 @@ var ( // Config represents the current projects configuration. type Config struct { - Version string `toml:"inertia"` - Project string `toml:"project"` - Remotes []*RemoteVPS `toml:"remote"` - Writer io.Writer `toml:"-"` + Version string `toml:"inertia"` + Project string `toml:"project-name"` + BuildType string `toml:"build-type"` + Remotes []*RemoteVPS `toml:"remote"` + Writer io.Writer `toml:"-"` } // Write writes configuration to Inertia config file. @@ -73,7 +74,7 @@ func (config *Config) RemoveRemote(name string) bool { // InitializeInertiaProject creates the inertia config folder and // returns an error if we're not in a git project. -func InitializeInertiaProject(version string) error { +func InitializeInertiaProject(version, buildType string) error { cwd, err := os.Getwd() if err != nil { return err @@ -82,25 +83,20 @@ func InitializeInertiaProject(version string) error { if err != nil { return err } - err = common.CheckForDockerCompose(cwd) - if err != nil { - return err - } - return createConfigFile(version) + return createConfigFile(version, buildType) } // createConfigFile returns an error if the config directory // already exists (the project is already initialized). -func createConfigFile(version string) error { +func createConfigFile(version, buildType string) error { configFilePath, err := GetConfigFilePath() if err != nil { return err } + // Check if Inertia is already set up. s, fileErr := os.Stat(configFilePath) - - // Check if everything already exists. if s != nil { return errors.New("inertia already properly configured in this folder") } @@ -110,12 +106,13 @@ func createConfigFile(version string) error { return err } - // Directory exists. Make sure JSON exists. + // Directory exists. Make sure configuration file exists. if os.IsNotExist(fileErr) { config := Config{ - Project: filepath.Base(cwd), - Version: version, - Remotes: make([]*RemoteVPS, 0), + Project: filepath.Base(cwd), + Version: version, + BuildType: buildType, + Remotes: make([]*RemoteVPS, 0), } path, err := GetConfigFilePath() diff --git a/client/config_test.go b/client/config_test.go index c8678a27..cf235c0f 100644 --- a/client/config_test.go +++ b/client/config_test.go @@ -8,7 +8,7 @@ import ( ) func TestConfigCreateAndWriteAndRead(t *testing.T) { - err := createConfigFile("") + err := createConfigFile("", "") assert.Nil(t, err) config, err := GetProjectConfigFromDisk() assert.Nil(t, err) diff --git a/client/deployment.go b/client/deployment.go index 113b660d..7f2dfdbc 100644 --- a/client/deployment.go +++ b/client/deployment.go @@ -20,6 +20,7 @@ type Deployment struct { Repository *git.Repository Auth string Project string + BuildType string } // GetDeployment returns the local deployment setup @@ -44,23 +45,29 @@ func GetDeployment(name string) (*Deployment, error) { RemoteVPS: remote, Repository: repo, Auth: auth, + BuildType: config.BuildType, Project: config.Project, }, nil } // Up brings the project up on the remote VPS instance specified // in the deployment object. -func (d *Deployment) Up(project string, stream bool) (*http.Response, error) { +func (d *Deployment) Up(buildType string, stream bool) (*http.Response, error) { // TODO: Support other Git remotes. origin, err := d.Repository.Remote("origin") if err != nil { return nil, err } + if buildType == "" { + buildType = d.BuildType + } + reqContent := &common.DaemonRequest{ - Stream: stream, - Project: project, - Secret: d.RemoteVPS.Daemon.Secret, + Stream: stream, + Project: d.Project, + BuildType: buildType, + Secret: d.RemoteVPS.Daemon.Secret, GitOptions: &common.GitOptions{ RemoteURL: common.GetSSHRemoteURL(origin.Config().URLs[0]), Branch: d.Branch, diff --git a/client/deployment_test.go b/client/deployment_test.go index c8826f62..9c09b5ee 100644 --- a/client/deployment_test.go +++ b/client/deployment_test.go @@ -22,7 +22,7 @@ var ( func getMockDeployment(ts *httptest.Server, s *memory.Storage) (*Deployment, error) { wholeURL := strings.Split(ts.URL, ":") url := strings.Trim(wholeURL[1], "/") - port := wholeURL[2] + port := wholeURL[2] mockRemote := &RemoteVPS{ User: "", IP: url, @@ -48,6 +48,7 @@ func getMockDeployment(ts *httptest.Server, s *memory.Storage) (*Deployment, err RemoteVPS: mockRemote, Repository: mockRepo, Auth: fakeAuth, + Project: "test_project", }, nil } @@ -68,6 +69,7 @@ func TestUp(t *testing.T) { assert.Equal(t, "myremote.git", upReq.GitOptions.RemoteURL) assert.Equal(t, "arjan", upReq.Secret) assert.Equal(t, "test_project", upReq.Project) + assert.Equal(t, "docker-compose", upReq.BuildType) // Check correct endpoint called endpoint := req.URL.Path @@ -84,7 +86,7 @@ func TestUp(t *testing.T) { d, err := getMockDeployment(testServer, memory) assert.Nil(t, err) - resp, err := d.Up("test_project", false) + resp, err := d.Up("docker-compose", false) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) } diff --git a/client/remote.go b/client/remote.go index 39464168..28a072e8 100644 --- a/client/remote.go +++ b/client/remote.go @@ -79,7 +79,9 @@ func (remote *RemoteVPS) Bootstrap(runner SSHSession, name string, config *Confi return err } - println("Inertia has been set up and daemon is running on remote!\n") + println("\nInertia has been set up and daemon is running on remote!") + println("You may have to wait briefly for Inertia to set up some dependencies.") + fmt.Printf("Use 'inertia %s logs --stream' to check on the daemon's setup progress.\n\n", name) println("=============================\n") diff --git a/common/request.go b/common/request.go index d0ed0d24..c22a9b20 100644 --- a/common/request.go +++ b/common/request.go @@ -1,8 +1,11 @@ package common const ( - // DaemonOkResp is the OK response upon successfully reaching daemon - DaemonOkResp = "I'm a little Webhook, short and stout!" + // DefaultSecret used for some verification + DefaultSecret = "inertia" + + // MsgDaemonOK is the OK response upon successfully reaching daemon + MsgDaemonOK = "I'm a little Webhook, short and stout!" // DefaultPort defines the standard daemon port DefaultPort = "8081" @@ -13,6 +16,7 @@ type DaemonRequest struct { Stream bool `json:"stream"` Container string `json:"container,omitempty"` Project string `json:"project"` + BuildType string `json:"build_type"` GitOptions *GitOptions `json:"git_options"` Secret string `json:"secret"` } diff --git a/common/util.go b/common/util.go index 654f8c3a..7eb33634 100644 --- a/common/util.go +++ b/common/util.go @@ -1,7 +1,6 @@ package common import ( - "errors" "io" "net/http" "os" @@ -10,18 +9,14 @@ import ( // CheckForDockerCompose returns error if current directory is a // not a docker-compose project -func CheckForDockerCompose(cwd string) error { +func CheckForDockerCompose(cwd string) bool { dockerComposeYML := filepath.Join(cwd, "docker-compose.yml") dockerComposeYAML := filepath.Join(cwd, "docker-compose.yaml") _, err := os.Stat(dockerComposeYML) - YMLpresent := os.IsNotExist(err) + YMLnotPresent := os.IsNotExist(err) _, err = os.Stat(dockerComposeYAML) - YAMLpresent := os.IsNotExist(err) - if YMLpresent && YAMLpresent { - return errors.New("this does not appear to be a docker-compose project - currently,\n" + - "Inertia only supports docker-compose projects.") - } - return nil + YAMLnotPresent := os.IsNotExist(err) + return !(YMLnotPresent && YAMLnotPresent) } // RemoveContents removes all files within given directory, returns nil if successful @@ -46,14 +41,21 @@ func RemoveContents(directory string) error { // FlushRoutine continuously writes everything in given ReadCloser // to a ResponseWriter. Use this as a goroutine. -func FlushRoutine(w io.Writer, rc io.ReadCloser) { +func FlushRoutine(w io.Writer, rc io.ReadCloser, stop chan struct{}) { buffer := make([]byte, 100) +ROUTINE: for { - // Read from pipe then write to ResponseWriter and flush it, - // sending the copied content to the client. - err := Flush(w, rc, buffer) - if err != nil { - break + select { + case <-stop: + Flush(w, rc, buffer) + break ROUTINE + default: + // Read from pipe then write to ResponseWriter and flush it, + // sending the copied content to the client. + err := Flush(w, rc, buffer) + if err != nil { + break ROUTINE + } } } } diff --git a/common/util_test.go b/common/util_test.go index c8a894cc..3380d1f1 100644 --- a/common/util_test.go +++ b/common/util_test.go @@ -18,27 +18,35 @@ func TestCheckForDockerCompose(t *testing.T) { cwd, err := os.Getwd() assert.Nil(t, err) - yamlPath := path.Join(cwd, "/docker-compose.yml") + ymlPath := path.Join(cwd, "/docker-compose.yml") + yamlPath := path.Join(cwd, "/docker-compose.yaml") - assert.NotEqual(t, nil, CheckForDockerCompose(cwd)) - file, err := os.Create(yamlPath) - assert.Nil(t, err) + // No! + b := CheckForDockerCompose(cwd) + assert.False(t, b) + // Yes! + file, err := os.Create(ymlPath) + assert.Nil(t, err) file.Close() - assert.Equal(t, nil, CheckForDockerCompose(cwd)) - os.Remove(yamlPath) + b = CheckForDockerCompose(cwd) + assert.True(t, b) + os.Remove(ymlPath) + + // Yes! file, err = os.Create(yamlPath) assert.Nil(t, err) file.Close() - - assert.Equal(t, nil, CheckForDockerCompose(cwd)) + b = CheckForDockerCompose(cwd) + assert.True(t, b) os.Remove(yamlPath) } func TestFlushRoutine(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { reader, writer := io.Pipe() - go FlushRoutine(w, reader) + stop := make(chan struct{}) + go FlushRoutine(w, reader, stop) fmt.Println(writer, "Hello!") time.Sleep(time.Millisecond) @@ -48,6 +56,9 @@ func TestFlushRoutine(t *testing.T) { fmt.Println(writer, "Bye!") time.Sleep(time.Millisecond) + + close(stop) + fmt.Println(writer, "Do I live?") })) defer testServer.Close() @@ -56,7 +67,7 @@ func TestFlushRoutine(t *testing.T) { reader := bufio.NewReader(resp.Body) i := 0 - for i < 3 { + for i < 4 { line, err := reader.ReadBytes('\n') if err != nil { break @@ -69,6 +80,8 @@ func TestFlushRoutine(t *testing.T) { assert.Equal(t, "Lunch?", string(line)) case 2: assert.Equal(t, "Bye!", string(line)) + case 3: + assert.Equal(t, "", string(line)) } i++ diff --git a/daemon/inertia/auth/auth.go b/daemon/inertia/auth/auth.go index 2c378fa4..ef3d063f 100644 --- a/daemon/inertia/auth/auth.go +++ b/daemon/inertia/auth/auth.go @@ -56,5 +56,5 @@ func GitAuthFailedErr() error { if err != nil { bytes = []byte(err.Error() + "\nError reading key - try running 'inertia [REMOTE] init' again: ") } - return errors.New("Access to project repository rejected; did you forget to add\nInertia's deploy key to your repository settings?\n" + string(bytes[:])) + return errors.New("Access to project repository rejected; did you forget to add\nInertia's deploy key to your repository settings?\n" + string(bytes)) } diff --git a/daemon/inertia/auth/auth_test.go b/daemon/inertia/auth/auth_test.go index 52b35fbe..17cb31f0 100644 --- a/daemon/inertia/auth/auth_test.go +++ b/daemon/inertia/auth/auth_test.go @@ -41,7 +41,7 @@ func TestAuthorizationOK(t *testing.T) { handler.ServeHTTP(rr, req) assert.Equal(t, rr.Code, http.StatusOK) - assert.Equal(t, rr.Body.String(), common.DaemonOkResp) + assert.Equal(t, rr.Body.String(), common.MsgDaemonOK) } func TestAuthorizationMalformedBearerString(t *testing.T) { diff --git a/daemon/inertia/auth/certificate.go b/daemon/inertia/auth/certificate.go index 42f46d99..90585888 100644 --- a/daemon/inertia/auth/certificate.go +++ b/daemon/inertia/auth/certificate.go @@ -11,7 +11,6 @@ package auth import ( "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -64,14 +63,6 @@ func GenerateCertificate(certPath, keyPath, host, method string) error { switch method { case "RSA": priv, err = rsa.GenerateKey(rand.Reader, rsaBits) - case "P224": - priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) - case "P256": - priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - case "P384": - priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - case "P521": - priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) default: return errors.New("Unrecognised algorithm " + method) } diff --git a/daemon/inertia/auth/handler.go b/daemon/inertia/auth/handler.go index 0215184d..ce3c5e1a 100644 --- a/daemon/inertia/auth/handler.go +++ b/daemon/inertia/auth/handler.go @@ -46,5 +46,5 @@ func Authorized(handler http.HandlerFunc, keyLookup func(*jwt.Token) (interface{ // HealthCheckHandler returns a 200 if the daemon is happy. func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - fmt.Fprint(w, common.DaemonOkResp) + fmt.Fprint(w, common.MsgDaemonOK) } diff --git a/daemon/inertia/main.go b/daemon/inertia/cmd.go similarity index 100% rename from daemon/inertia/main.go rename to daemon/inertia/cmd.go diff --git a/daemon/inertia/daemon.go b/daemon/inertia/daemon.go index c6c72a03..b83a0587 100644 --- a/daemon/inertia/daemon.go +++ b/daemon/inertia/daemon.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "os" + "sync" "github.com/docker/docker/api/types" docker "github.com/docker/docker/client" @@ -11,10 +12,17 @@ import ( "github.com/ubclaunchpad/inertia/daemon/inertia/project" ) -// daemonVersion indicates the daemon's corresponding Inertia daemonVersion -var daemonVersion string +var ( + // daemonVersion indicates the daemon's corresponding Inertia daemonVersion + daemonVersion string + + // deployment is the currently deployed project on this remote + deployment project.Deployer +) const ( + msgNoDeployment = "No deployment is currently active on this remote - try running 'inertia $REMOTE up'" + // specify location of SSL certificate sslDirectory = "/app/host/ssl/" daemonSSLCert = sslDirectory + "daemon.cert" @@ -25,22 +33,15 @@ const ( func run(host, port, version string) { daemonVersion = version - // Download docker-compose image - println("Downloading docker-compose...") + // Download build tools cli, err := docker.NewEnvClient() if err != nil { println(err.Error()) println("Failed to start Docker client - shutting down daemon.") return } - _, err = cli.ImagePull(context.Background(), project.DockerComposeVersion, types.ImagePullOptions{}) - if err != nil { - println(err.Error()) - println("Failed to pull docker-compose image - shutting down daemon.") - cli.Close() - return - } - cli.Close() + println("Downloading build tools...") + go downloadDeps(cli) // Check if the cert files are available. println("Checking for existing SSL certificates in " + sslDirectory + "...") @@ -84,3 +85,23 @@ func run(host, port, version string) { mux, )) } + +func downloadDeps(cli *docker.Client) { + var wait sync.WaitGroup + wait.Add(2) + go dockerPull(project.DockerComposeVersion, cli, &wait) + go dockerPull(project.HerokuishVersion, cli, &wait) + wait.Wait() + cli.Close() +} + +func dockerPull(image string, cli *docker.Client, wait *sync.WaitGroup) { + defer wait.Done() + println("Downloading " + image) + _, err := cli.ImagePull(context.Background(), image, types.ImagePullOptions{}) + if err != nil { + println(err.Error()) + } else { + println(image + " download complete") + } +} diff --git a/daemon/inertia/down.go b/daemon/inertia/down.go index f4554854..b4254549 100644 --- a/daemon/inertia/down.go +++ b/daemon/inertia/down.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "net/http" docker "github.com/docker/docker/client" @@ -10,38 +9,29 @@ import ( // downHandler tries to take the deployment offline func downHandler(w http.ResponseWriter, r *http.Request) { - println("DOWN request received") + if deployment == nil { + http.Error(w, msgNoDeployment, http.StatusPreconditionFailed) + return + } logger := newLogger(false, w) defer logger.Close() cli, err := docker.NewEnvClient() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Err(err.Error(), http.StatusPreconditionFailed) return } defer cli.Close() - // Error if no project containers are active, but try to kill - // everything anyway in case the docker-compose image is still - // active - _, err = project.GetActiveContainers(cli) - if err != nil { - http.Error(w, err.Error(), http.StatusPreconditionFailed) - err = project.KillActiveContainers(cli, logger.GetWriter()) - if err != nil { - println(err) - } + err = deployment.Down(cli, logger.GetWriter()) + if err == project.ErrNoContainers { + logger.Err(err.Error(), http.StatusPreconditionFailed) return - } - - err = project.KillActiveContainers(cli, logger.GetWriter()) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + } else if err != nil { + logger.Err(err.Error(), http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "Project shut down.") + logger.Success("Project shut down.", http.StatusOK) } diff --git a/daemon/inertia/down_test.go b/daemon/inertia/down_test.go new file mode 100644 index 00000000..9e9bf4f6 --- /dev/null +++ b/daemon/inertia/down_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLogHandlerNoDeployment(t *testing.T) { + // Assmble request + req, err := http.NewRequest("POST", "/down", nil) + assert.Nil(t, err) + + // Record responses + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(downHandler) + + handler.ServeHTTP(recorder, req) + assert.Equal(t, recorder.Code, http.StatusPreconditionFailed) + assert.Contains(t, recorder.Body.String(), msgNoDeployment) +} diff --git a/daemon/inertia/logger.go b/daemon/inertia/logger.go index 5d323599..11525c95 100644 --- a/daemon/inertia/logger.go +++ b/daemon/inertia/logger.go @@ -24,41 +24,33 @@ func (d *daemonWriter) Write(p []byte) (n int, err error) { return d.stdWriter.Write(p) } -// logger is a multilogger used by the daemon to pipe +// daemonLogger is a multilogger used by the daemon to pipe // output to multiple places depending on context. -type logger interface { - Println(a interface{}) - Err(msg string, status int) - Success(msg string, status int) - GetWriter() io.Writer - Close() -} - -// daemonLogger is the default logger implementation used -// by the Inertia daemon type daemonLogger struct { stream bool writer io.Writer reader *io.PipeReader httpWriter http.ResponseWriter + stop chan struct{} } // newLogger creates a new logger -func newLogger(stream bool, httpWriter http.ResponseWriter) logger { +func newLogger(stream bool, httpWriter http.ResponseWriter) *daemonLogger { writer := &daemonWriter{stdWriter: os.Stdout} - var reader *io.PipeReader - if stream { - r, w := io.Pipe() - go common.FlushRoutine(httpWriter, r) - writer.strWriter = w - reader = r - } - return &daemonLogger{ + logger := &daemonLogger{ stream: stream, writer: writer, - reader: reader, httpWriter: httpWriter, } + if stream { + r, w := io.Pipe() + stop := make(chan struct{}) + go common.FlushRoutine(httpWriter, r, stop) + writer.strWriter = w + logger.reader = r + logger.stop = stop + } + return logger } // Println prints to logger's standard writer @@ -93,5 +85,6 @@ func (l *daemonLogger) GetWriter() io.Writer { func (l *daemonLogger) Close() { if l.stream { l.reader.Close() + close(l.stop) } } diff --git a/daemon/inertia/logs.go b/daemon/inertia/logs.go index 83be8580..d8270926 100644 --- a/daemon/inertia/logs.go +++ b/daemon/inertia/logs.go @@ -2,21 +2,17 @@ package main import ( "bytes" - "context" "encoding/json" - "fmt" "io/ioutil" "net/http" - "github.com/docker/docker/api/types" docker "github.com/docker/docker/client" "github.com/ubclaunchpad/inertia/common" + "github.com/ubclaunchpad/inertia/daemon/inertia/project" ) // logHandler handles requests for container logs func logHandler(w http.ResponseWriter, r *http.Request) { - println("LOG request received") - // Get container name from request body, err := ioutil.ReadAll(r.Body) if err != nil { @@ -34,32 +30,38 @@ func logHandler(w http.ResponseWriter, r *http.Request) { logger := newLogger(upReq.Stream, w) defer logger.Close() + if container != "/inertia-daemon" && deployment == nil { + logger.Err(msgNoDeployment, http.StatusPreconditionFailed) + return + } + cli, err := docker.NewEnvClient() if err != nil { logger.Err(err.Error(), http.StatusInternalServerError) return } defer cli.Close() - ctx := context.Background() - logs, err := cli.ContainerLogs(ctx, container, types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Follow: upReq.Stream, - Timestamps: true, - }) + + logs, err := deployment.Logs(project.LogOptions{ + Container: upReq.Container, + Stream: upReq.Stream, + }, cli) if err != nil { - logger.Err(err.Error(), http.StatusInternalServerError) + if docker.IsErrContainerNotFound(err) { + logger.Err(err.Error(), http.StatusNotFound) + } else { + logger.Err(err.Error(), http.StatusInternalServerError) + } return } defer logs.Close() if upReq.Stream { - common.FlushRoutine(w, logs) + stop := make(chan struct{}) + common.FlushRoutine(w, logs, stop) } else { buf := new(bytes.Buffer) buf.ReadFrom(logs) - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, buf.String()) + logger.Success(buf.String(), http.StatusOK) } } diff --git a/daemon/inertia/mock_test.go b/daemon/inertia/mock_test.go new file mode 100644 index 00000000..8d7c8f9e --- /dev/null +++ b/daemon/inertia/mock_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "io" + + docker "github.com/docker/docker/client" + "github.com/ubclaunchpad/inertia/daemon/inertia/project" +) + +// This file contains mock implementations of interfaces used by this +// package for testing purposes. + +// FakeDeployment is an implementation of the project.Deployer interface. +// Make sure to assign functions to each field that gets called or a nil +// pointer will be thrown. +type FakeDeployment struct { + CompareRemotesFunc func(in1 string) error + DeployFunc func(in1 project.DeployOptions, in2 *docker.Client, in3 io.Writer) error + DestroyFunc func(in1 *docker.Client, in2 io.Writer) error + DownFunc func(in1 *docker.Client, in2 io.Writer) error + GetBranchFunc func() string + GetStatusFunc func(in1 *docker.Client) (*project.DeploymentStatus, error) + LogsFunc func(in1 project.LogOptions, in2 *docker.Client) (io.ReadCloser, error) +} + +func (f *FakeDeployment) Deploy(o project.DeployOptions, c *docker.Client, w io.Writer) error { + return f.DeployFunc(o, c, w) +} + +func (f *FakeDeployment) Down(c *docker.Client, w io.Writer) error { + return f.DownFunc(c, w) +} + +func (f *FakeDeployment) Destroy(c *docker.Client, w io.Writer) error { + return f.DestroyFunc(c, w) +} + +func (f *FakeDeployment) Logs(o project.LogOptions, c *docker.Client) (io.ReadCloser, error) { + return f.LogsFunc(o, c) +} + +func (f *FakeDeployment) GetStatus(c *docker.Client) (*project.DeploymentStatus, error) { + return f.GetStatusFunc(c) +} + +func (f *FakeDeployment) SetConfig(project.DeploymentConfig) {} + +func (f *FakeDeployment) GetBranch() string { + return f.GetBranchFunc() +} + +func (f *FakeDeployment) CompareRemotes(s string) error { + return f.CompareRemotesFunc(s) +} diff --git a/daemon/inertia/project/deployment.go b/daemon/inertia/project/deployment.go new file mode 100644 index 00000000..a4a728da --- /dev/null +++ b/daemon/inertia/project/deployment.go @@ -0,0 +1,391 @@ +package project + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "strconv" + "strings" + "sync" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + docker "github.com/docker/docker/client" + "github.com/ubclaunchpad/inertia/common" + "github.com/ubclaunchpad/inertia/daemon/inertia/auth" + git "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh" +) + +const ( + // DockerComposeVersion is the version of docker-compose used + DockerComposeVersion = "docker/compose:1.19.0" + + // HerokuishVersion is the version of Herokuish used + HerokuishVersion = "gliderlabs/herokuish:v0.4.0" + + // Directory specifies the location of deployed project + Directory = "/app/host/project" + + // BuildStageName specifies the name of build stage containers + BuildStageName = "build" +) + +// Deployer does great deploys +type Deployer interface { + Deploy(DeployOptions, *docker.Client, io.Writer) error + Down(*docker.Client, io.Writer) error + Destroy(*docker.Client, io.Writer) error + + Logs(LogOptions, *docker.Client) (io.ReadCloser, error) + GetStatus(*docker.Client) (*DeploymentStatus, error) + + SetConfig(DeploymentConfig) + GetBranch() string + CompareRemotes(string) error +} + +// Deployment represents the deployed project +type Deployment struct { + project string + branch string + buildType string + + repo *git.Repository + auth ssh.AuthMethod + mux sync.Mutex +} + +// DeploymentConfig is used to configure Deployment +type DeploymentConfig struct { + ProjectName string + BuildType string + RemoteURL string + Branch string + PemFilePath string +} + +// NewDeployment creates a new deployment +func NewDeployment(cfg DeploymentConfig, out io.Writer) (*Deployment, error) { + pemFile, err := os.Open(cfg.PemFilePath) + if err != nil { + return nil, err + } + authMethod, err := auth.GetGithubKey(pemFile) + if err != nil { + return nil, err + } + repo, err := initializeRepository(cfg.RemoteURL, cfg.Branch, authMethod, out) + if err != nil { + return nil, err + } + + return &Deployment{ + project: cfg.ProjectName, + branch: cfg.Branch, + buildType: cfg.BuildType, + auth: authMethod, + repo: repo, + }, nil +} + +// SetConfig updates the deployment's configuration. Only supports +// ProjectName, Branch, and BuildType for now. +func (d *Deployment) SetConfig(cfg DeploymentConfig) { + if cfg.ProjectName != "" { + d.project = cfg.ProjectName + } + if cfg.Branch != "" { + d.branch = cfg.Branch + } + if cfg.BuildType != "" { + d.buildType = cfg.BuildType + } +} + +// DeployOptions is used to configure how the deployment handles the deploy +type DeployOptions struct { + SkipUpdate bool +} + +// Deploy will update, build, and deploy the project +func (d *Deployment) Deploy(opts DeployOptions, cli *docker.Client, out io.Writer) error { + d.mux.Lock() + defer d.mux.Unlock() + fmt.Println(out, "Preparing to deploy project") + + // Update repository + if !opts.SkipUpdate { + err := updateRepository(Directory, d.repo, d.branch, d.auth, out) + if err != nil { + return err + } + } + + // Kill active project containers if there are any + err := stopActiveContainers(cli, out) + if err != nil { + return err + } + + // Use the appropriate build method + switch d.buildType { + case "herokuish": + return d.herokuishBuild(cli, out) + case "docker-compose": + return d.dockerCompose(cli, out) + default: + fmt.Println(out, "Unknown project type "+d.buildType) + fmt.Println(out, "Defaulting to docker-compose build") + return d.dockerCompose(cli, out) + } +} + +// Down shuts down the deployment +func (d *Deployment) Down(cli *docker.Client, out io.Writer) error { + d.mux.Lock() + defer d.mux.Unlock() + + // Error if no project containers are active, but try to kill + // everything anyway in case the docker-compose image is still + // active + _, err := getActiveContainers(cli) + if err != nil { + killErr := stopActiveContainers(cli, out) + if killErr != nil { + println(err) + } + return err + } + return stopActiveContainers(cli, out) +} + +// Destroy shuts down the deployment and removes the repository +func (d *Deployment) Destroy(cli *docker.Client, out io.Writer) error { + d.Down(cli, out) + + d.mux.Lock() + defer d.mux.Unlock() + return common.RemoveContents(Directory) +} + +// LogOptions is used to configure retrieved container logs +type LogOptions struct { + Container string + Stream bool +} + +// Logs get logs ;) +func (d *Deployment) Logs(opts LogOptions, cli *docker.Client) (io.ReadCloser, error) { + ctx := context.Background() + return cli.ContainerLogs(ctx, opts.Container, types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: opts.Stream, + Timestamps: true, + }) +} + +// DeploymentStatus lists details about the deployed project +type DeploymentStatus struct { + Branch string + CommitHash string + CommitMessage string + BuildType string + Containers []string + BuildContainerActive bool +} + +// GetStatus returns the status of the deployment +func (d *Deployment) GetStatus(cli *docker.Client) (*DeploymentStatus, error) { + // Get repository status + head, err := d.repo.Head() + if err != nil { + return nil, err + } + commit, err := d.repo.CommitObject(head.Hash()) + if err != nil { + return nil, err + } + + // Get containers, filtering out non-project containers + buildContainerActive := false + containers, err := getActiveContainers(cli) + if err != nil && err != ErrNoContainers { + return nil, err + } + ignore := map[string]bool{ + "/inertia-daemon": true, + "/" + BuildStageName: true, + } + activeContainers := make([]string, 0) + for _, container := range containers { + if !ignore[container.Names[0]] { + activeContainers = append(activeContainers, container.Names[0]) + } else { + if container.Names[0] == "/docker-compose" { + buildContainerActive = true + } + } + } + + return &DeploymentStatus{ + Branch: strings.TrimSpace(head.Name().Short()), + CommitHash: strings.TrimSpace(head.Hash().String()), + CommitMessage: strings.TrimSpace(commit.Message), + BuildType: strings.TrimSpace(d.buildType), + Containers: activeContainers, + BuildContainerActive: buildContainerActive, + }, nil +} + +// GetBranch returns the currently deployed branch +func (d *Deployment) GetBranch() string { + return d.branch +} + +// CompareRemotes will compare the remote of the deployment +// with given remote URL and return nil if they match +func (d *Deployment) CompareRemotes(remoteURL string) error { + remotes, err := d.repo.Remotes() + if err != nil { + return err + } + localRemoteURL := common.GetSSHRemoteURL(remotes[0].Config().URLs[0]) + if localRemoteURL != common.GetSSHRemoteURL(remoteURL) { + return errors.New("The given remote URL does not match that of the repository in\nyour remote - try 'inertia [REMOTE] reset'") + } + return nil +} + +// dockerCompose builds and runs project using docker-compose - +// the following code performs the bash equivalent of: +// +// docker run -d \ +// -v /var/run/docker.sock:/var/run/docker.sock \ +// -v $HOME:/build \ +// -w="/build/project" \ +// docker/compose:1.18.0 up --build +// +// This starts a new container running a docker-compose image for +// the sole purpose of building the project. This container is +// separate from the daemon and the user's project, and is the +// second container to require access to the docker socket. +// See https://cloud.google.com/community/tutorials/docker-compose-on-container-optimized-os +func (d *Deployment) dockerCompose(cli *docker.Client, out io.Writer) error { + fmt.Fprintln(out, "Setting up docker-compose...") + ctx := context.Background() + resp, err := cli.ContainerCreate( + ctx, &container.Config{ + Image: DockerComposeVersion, + WorkingDir: "/build/project", + Env: []string{"HOME=/build"}, + Cmd: []string{ + // set project name + "-p", d.project, + // run "up" with flags + "up", "--build", + }, + }, + &container.HostConfig{ + AutoRemove: true, + Binds: []string{ + os.Getenv("HOME") + ":/build", + // docker-compose needs to be able to start other containers + "/var/run/docker.sock:/var/run/docker.sock", + }, + }, nil, BuildStageName, + ) + if err != nil { + return err + } + if len(resp.Warnings) > 0 { + warnings := strings.Join(resp.Warnings, "\n") + return errors.New(warnings) + } + + fmt.Fprintln(out, "Building and starting up project...") + return cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) +} + +// herokuishBuild uses the Herokuish tool to use Heroku's official buidpacks +// to build the user project. +func (d *Deployment) herokuishBuild(cli *docker.Client, out io.Writer) error { + fmt.Fprintln(out, "Setting up herokuish...") + ctx := context.Background() + + // Configure herokuish container to build project when run + resp, err := cli.ContainerCreate( + ctx, &container.Config{ + Image: HerokuishVersion, + Cmd: []string{"/build"}, + }, + &container.HostConfig{ + Binds: []string{ + // "/tmp/app" is the directory herokuish looks + // for during a build, so mount project there + os.Getenv("HOME") + "/project:/tmp/app", + }, + }, nil, BuildStageName, + ) + if err != nil { + return err + } + if len(resp.Warnings) > 0 { + fmt.Fprintln(out, "Warnings encountered on herokuish setup.") + warnings := strings.Join(resp.Warnings, "\n") + return errors.New(warnings) + } + + // Start the herokuish container to build project + fmt.Fprintln(out, "Building project...") + err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) + if err != nil { + return err + } + + // Attach logs and report build progress until container exits + reader, err := d.Logs(LogOptions{Container: resp.ID, Stream: true}, cli) + if err != nil { + return err + } + stop := make(chan struct{}) + go common.FlushRoutine(out, reader, stop) + status, err := cli.ContainerWait(ctx, resp.ID) + close(stop) + reader.Close() + if err != nil { + return err + } + if status != 0 { + return errors.New("Build exited with non-zero status: " + strconv.FormatInt(status, 10)) + } + fmt.Fprintln(out, "Build exited with status "+strconv.FormatInt(status, 10)) + + // Save build as new image and create a container + fmt.Fprintln(out, "Saving build...") + _, err = cli.ContainerCommit(ctx, resp.ID, types.ContainerCommitOptions{ + Reference: "inertia-build", + }) + if err != nil { + return err + } + resp, err = cli.ContainerCreate(ctx, &container.Config{ + Image: "inertia-build:latest", + // Currently, only start the standard "web" process + Cmd: []string{"/start", "web"}, + }, nil, nil, d.project) + if err != nil { + return err + } + if len(resp.Warnings) > 0 { + fmt.Fprintln(out, "Warnings encountered on herokuish startup.") + warnings := strings.Join(resp.Warnings, "\n") + return errors.New(warnings) + } + + fmt.Fprintln(out, "Starting up project in container "+d.project+"...") + return cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) +} diff --git a/daemon/inertia/project/deployment_test.go b/daemon/inertia/project/deployment_test.go new file mode 100644 index 00000000..b16e0fd2 --- /dev/null +++ b/daemon/inertia/project/deployment_test.go @@ -0,0 +1,21 @@ +package project + +import ( + "testing" + + "github.com/stretchr/testify/assert" + git "gopkg.in/src-d/go-git.v4" +) + +func TestCompareRemotes(t *testing.T) { + // Traverse back down to root directory of repository + repo, err := git.PlainOpen("../../../") + assert.Nil(t, err) + + deployment := &Deployment{repo: repo} + + for _, url := range urlVariations { + err = deployment.CompareRemotes(url) + assert.Nil(t, err) + } +} diff --git a/daemon/inertia/project/docker.go b/daemon/inertia/project/docker.go index f656b9b7..060a050d 100644 --- a/daemon/inertia/project/docker.go +++ b/daemon/inertia/project/docker.go @@ -5,116 +5,21 @@ import ( "errors" "fmt" "io" - "os" - "strings" "time" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" docker "github.com/docker/docker/client" - git "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh" ) -const ( - // Directory specifies the location of deployed project - Directory = "/app/host/project" - - // DockerComposeVersion is the docker-compose version used by the daemon - DockerComposeVersion = "docker/compose:1.18.0" - - // NoContainersResp is the response to indicate that no containers are active - NoContainersResp = "There are currently no active containers." +var ( + // ErrNoContainers is the response to indicate that no containers are active + ErrNoContainers = errors.New("There are currently no active containers") ) -// ProjectName is user-assigned name of deployed project -var ProjectName = "project" - -// Deploy does git pull, docker-compose build, docker-compose up -func Deploy(auth ssh.AuthMethod, repo *git.Repository, branch string, project string, cli *docker.Client, out io.Writer) error { - fmt.Println(out, "Deploying repository...") - - // set up global projectName for other calls to Deploy - ProjectName = project - - // Pull from given branch and check out if needed - err := UpdateRepository(Directory, repo, branch, auth, out) - if err != nil { - return err - } - - // Kill active project containers if there are any - err = KillActiveContainers(cli, out) - if err != nil { - return err - } - - // Build and run project - the following code performs the bash - // equivalent of: - // - // docker run -d \ - // -v /var/run/docker.sock:/var/run/docker.sock \ - // -v $HOME:/build \ - // -w="/build/project" \ - // docker/compose:1.18.0 up --build - // - // This starts a new container running a docker-compose image for - // the sole purpose of building the project. This container is - // separate from the daemon and the user's project, and is the - // second container to require access to the docker socket. - // See https://cloud.google.com/community/tutorials/docker-compose-on-container-optimized-os - fmt.Fprintln(out, "Setting up docker-compose...") - ctx := context.Background() - resp, err := cli.ContainerCreate( - ctx, &container.Config{ - Image: DockerComposeVersion, - WorkingDir: "/build/project", - Env: []string{"HOME=/build"}, - Cmd: []string{ - "-p", project, - "up", - "--build", - }, - }, - &container.HostConfig{ - Binds: []string{ - "/var/run/docker.sock:/var/run/docker.sock", - os.Getenv("HOME") + ":/build", - }, - }, nil, "docker-compose", - ) - if err != nil { - return err - } - if len(resp.Warnings) > 0 { - warnings := strings.Join(resp.Warnings, "\n") - return errors.New(warnings) - } - - fmt.Fprintln(out, "Building project...") - return cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) - - // Check if build failed abruptly - // This is disabled until a more consistent way of detecting build - // failures is implemented. - /* - time.Sleep(3 * time.Second) - _, err = getActiveContainers(cli) - if err != nil { - killErr := killActiveContainers(cli, out) - if killErr != nil { - fmt.Fprintln(out, err) - } - return errors.New("Docker-compose failed: " + err.Error()) - } - return nil - */ -} - -// GetActiveContainers returns all active containers and returns and error +// getActiveContainers returns all active containers and returns and error // if the Daemon is the only active container -func GetActiveContainers(cli *docker.Client) ([]types.Container, error) { +func getActiveContainers(cli *docker.Client) ([]types.Container, error) { containers, err := cli.ContainerList( context.Background(), types.ContainerListOptions{}, @@ -125,14 +30,14 @@ func GetActiveContainers(cli *docker.Client) ([]types.Container, error) { // Error if only one container (daemon) is active if len(containers) <= 1 { - return nil, errors.New(NoContainersResp) + return nil, ErrNoContainers } return containers, nil } -// KillActiveContainers kills all active project containers (ie not including daemon) -func KillActiveContainers(cli *docker.Client, out io.Writer) error { +// stopActiveContainers kills all active project containers (ie not including daemon) +func stopActiveContainers(cli *docker.Client, out io.Writer) error { fmt.Fprintln(out, "Shutting down active containers...") ctx := context.Background() containers, err := cli.ContainerList(ctx, types.ContainerListOptions{}) @@ -140,9 +45,10 @@ func KillActiveContainers(cli *docker.Client, out io.Writer) error { return err } + // Gracefully take down all containers except the daemon for _, container := range containers { if container.Names[0] != "/inertia-daemon" { - fmt.Fprintln(out, "Killing "+container.Image+" ("+container.Names[0]+")...") + fmt.Fprintln(out, "Stopping "+container.Names[0]+"...") timeout := 10 * time.Second err := cli.ContainerStop(ctx, container.ID, &timeout) if err != nil { @@ -151,12 +57,7 @@ func KillActiveContainers(cli *docker.Client, out io.Writer) error { } } - report, err := cli.ContainersPrune(ctx, filters.Args{}) - if err != nil { - return err - } - if len(report.ContainersDeleted) > 0 { - fmt.Fprintln(out, "Removed "+strings.Join(report.ContainersDeleted, ", ")) - } - return nil + // Prune images + _, err = cli.ContainersPrune(ctx, filters.Args{}) + return err } diff --git a/daemon/inertia/project/repository.go b/daemon/inertia/project/git.go similarity index 70% rename from daemon/inertia/project/repository.go rename to daemon/inertia/project/git.go index a73e29b5..6a612872 100644 --- a/daemon/inertia/project/repository.go +++ b/daemon/inertia/project/git.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "io" - "os" "strings" "github.com/ubclaunchpad/inertia/common" @@ -21,29 +20,6 @@ var ( ErrInvalidGitAuthentication = errors.New("git authentication failed") ) -// InitializeRepository sets up a project repository for the first time -func InitializeRepository(remoteURL, branch string, w io.Writer) error { - fmt.Fprintln(w, "Setting up project...") - pemFile, err := os.Open(auth.DaemonGithubKeyLocation) - if err != nil { - return err - } - authMethod, err := auth.GetGithubKey(pemFile) - if err != nil { - return err - } - - // Clone project - _, err = Clone(Directory, remoteURL, branch, authMethod, w) - if err != nil { - if err == ErrInvalidGitAuthentication { - return auth.GitAuthFailedErr() - } - return err - } - return nil -} - // SimplifyGitErr checks errors that involve git remote operations and simplifies them // to ErrInvalidGitAuthentication if possible func SimplifyGitErr(err error) error { @@ -56,9 +32,23 @@ func SimplifyGitErr(err error) error { return nil } -// Clone wraps git.PlainClone() and returns a more helpful error message +// initializeRepository sets up a project repository for the first time +func initializeRepository(remoteURL, branch string, authMethod ssh.AuthMethod, w io.Writer) (*git.Repository, error) { + fmt.Fprintln(w, "Setting up project...") + // Clone project + repo, err := clone(Directory, remoteURL, branch, authMethod, w) + if err != nil { + if err == ErrInvalidGitAuthentication { + return nil, auth.GitAuthFailedErr() + } + return nil, err + } + return repo, nil +} + +// clone wraps git.PlainClone() and returns a more helpful error message // if the given error is an authentication-related error. -func Clone(directory, remoteURL, branch string, auth ssh.AuthMethod, out io.Writer) (*git.Repository, error) { +func clone(directory, remoteURL, branch string, auth ssh.AuthMethod, out io.Writer) (*git.Repository, error) { fmt.Fprintf(out, "Cloning branch %s from %s...\n", branch, remoteURL) ref := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", branch)) repo, err := git.PlainClone(directory, false, &git.CloneOptions{ @@ -80,9 +70,9 @@ func Clone(directory, remoteURL, branch string, auth ssh.AuthMethod, out io.Writ return repo, nil } -// ForcePull deletes the project directory and makes a fresh clone of given repo +// forcePull deletes the project directory and makes a fresh clone of given repo // git.Worktree.Pull() only supports merges that can be resolved as a fast-forward -func ForcePull(directory string, repo *git.Repository, auth ssh.AuthMethod, out io.Writer) (*git.Repository, error) { +func forcePull(directory string, repo *git.Repository, auth ssh.AuthMethod, out io.Writer) (*git.Repository, error) { fmt.Fprintln(out, "Making a force pull...") remotes, err := repo.Remotes() if err != nil { @@ -99,15 +89,15 @@ func ForcePull(directory string, repo *git.Repository, auth ssh.AuthMethod, out if err != nil { return nil, err } - repo, err = Clone(directory, remoteURL, branch, auth, out) + repo, err = clone(directory, remoteURL, branch, auth, out) if err != nil { return nil, err } return repo, nil } -// UpdateRepository pulls and checkouts given branch from repository -func UpdateRepository(directory string, repo *git.Repository, branch string, auth ssh.AuthMethod, out io.Writer) error { +// updateRepository pulls and checkouts given branch from repository +func updateRepository(directory string, repo *git.Repository, branch string, auth ssh.AuthMethod, out io.Writer) error { tree, err := repo.Worktree() if err != nil { return err @@ -146,7 +136,7 @@ func UpdateRepository(directory string, repo *git.Repository, branch string, aut if err == git.ErrForceNeeded { // If pull fails, attempt a force pull before returning error fmt.Fprintln(out, "Fast-forward failed - a force pull is required.") - _, err := ForcePull(directory, repo, auth, out) + _, err := forcePull(directory, repo, auth, out) if err != nil { return err } @@ -156,16 +146,3 @@ func UpdateRepository(directory string, repo *git.Repository, branch string, aut } return nil } - -// CompareRemotes checks if the given remote matches the remote of the given repository -func CompareRemotes(localRepo *git.Repository, remoteURL string) error { - remotes, err := localRepo.Remotes() - if err != nil { - return err - } - localRemoteURL := common.GetSSHRemoteURL(remotes[0].Config().URLs[0]) - if localRemoteURL != common.GetSSHRemoteURL(remoteURL) { - return errors.New("The given remote URL does not match that of the repository in\nyour remote - try 'inertia [REMOTE] reset'") - } - return nil -} diff --git a/daemon/inertia/project/repository_test.go b/daemon/inertia/project/git_test.go similarity index 74% rename from daemon/inertia/project/repository_test.go rename to daemon/inertia/project/git_test.go index 22140490..5b8093e3 100644 --- a/daemon/inertia/project/repository_test.go +++ b/daemon/inertia/project/git_test.go @@ -39,7 +39,7 @@ func TestCloneIntegration(t *testing.T) { } dir := "./test_clone/" - repo, err := Clone(dir, inertiaDeployTest, "dev", nil, os.Stdout) + repo, err := clone(dir, inertiaDeployTest, "dev", nil, os.Stdout) defer os.RemoveAll(dir) assert.Nil(t, err) @@ -61,13 +61,13 @@ func TestForcePullIntegration(t *testing.T) { }) defer os.RemoveAll(dir) assert.Nil(t, err) - forcePulledRepo, err := ForcePull(dir, repo, auth, os.Stdout) + forcePulledRepo, err := forcePull(dir, repo, auth, os.Stdout) assert.Nil(t, err) // Try switching branches - err = UpdateRepository(dir, forcePulledRepo, "dev", auth, os.Stdout) + err = updateRepository(dir, forcePulledRepo, "dev", auth, os.Stdout) assert.Nil(t, err) - err = UpdateRepository(dir, forcePulledRepo, "master", auth, os.Stdout) + err = updateRepository(dir, forcePulledRepo, "master", auth, os.Stdout) assert.Nil(t, err) } @@ -84,19 +84,8 @@ func TestUpdateRepositoryIntegration(t *testing.T) { assert.Nil(t, err) // Try switching branches - err = UpdateRepository(dir, repo, "master", nil, os.Stdout) + err = updateRepository(dir, repo, "master", nil, os.Stdout) assert.Nil(t, err) - err = UpdateRepository(dir, repo, "dev", nil, os.Stdout) + err = updateRepository(dir, repo, "dev", nil, os.Stdout) assert.Nil(t, err) } - -func TestCompareRemotes(t *testing.T) { - // Traverse back down to root directory of repository - repo, err := git.PlainOpen("../../../") - assert.Nil(t, err) - - for _, url := range urlVariations { - err = CompareRemotes(repo, url) - assert.Nil(t, err) - } -} diff --git a/daemon/inertia/reset.go b/daemon/inertia/reset.go index 98ac30f6..6a10b9cd 100644 --- a/daemon/inertia/reset.go +++ b/daemon/inertia/reset.go @@ -1,40 +1,35 @@ package main import ( - "fmt" "net/http" docker "github.com/docker/docker/client" - "github.com/ubclaunchpad/inertia/common" - "github.com/ubclaunchpad/inertia/daemon/inertia/project" ) // resetHandler shuts down and wipes the project directory func resetHandler(w http.ResponseWriter, r *http.Request) { - println("RESET request received") + if deployment == nil { + http.Error(w, msgNoDeployment, http.StatusPreconditionFailed) + return + } logger := newLogger(false, w) defer logger.Close() cli, err := docker.NewEnvClient() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Err(err.Error(), http.StatusInternalServerError) return } defer cli.Close() - err = project.KillActiveContainers(cli, logger.GetWriter()) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - err = common.RemoveContents(project.Directory) + // Goodbye deployment + err = deployment.Destroy(cli, logger.GetWriter()) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Err(err.Error(), http.StatusInternalServerError) return } + deployment = nil - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "Project removed from remote.") + logger.Success("Project removed from remote.", http.StatusOK) } diff --git a/daemon/inertia/status.go b/daemon/inertia/status.go index ddee20a0..f2329160 100644 --- a/daemon/inertia/status.go +++ b/daemon/inertia/status.go @@ -5,80 +5,64 @@ import ( "net/http" docker "github.com/docker/docker/client" - "github.com/ubclaunchpad/inertia/daemon/inertia/project" - git "gopkg.in/src-d/go-git.v4" ) -// statusHandler lists currently active project containers -func statusHandler(w http.ResponseWriter, r *http.Request) { - println("STATUS request received") +const ( + msgBuildInProgress = "It appears that your build is still in progress." + msgNoContainersActive = "No containers are active." +) +// statusHandler returns a formatted string about the status of the +// deployment and lists currently active project containers +func statusHandler(w http.ResponseWriter, r *http.Request) { inertiaStatus := "inertia daemon " + daemonVersion + "\n" - - // Get status of repository - repo, err := git.PlainOpen(project.Directory) - if err != nil { - http.Error(w, err.Error(), http.StatusPreconditionFailed) - return - } - head, err := repo.Head() - if err != nil { - http.Error(w, err.Error(), http.StatusPreconditionFailed) + if deployment == nil { + http.Error( + w, inertiaStatus+msgNoDeployment, + http.StatusNotFound, + ) return } - commit, err := repo.CommitObject(head.Hash()) - if err != nil { - return - } - branchStatus := " - Branch: " + head.Name().Short() + "\n" - commitStatus := " - Commit: " + head.Hash().String() + "\n" - commitMessage := " - Message: " + commit.Message + "\n" - status := inertiaStatus + branchStatus + commitStatus + commitMessage - // Get containers cli, err := docker.NewEnvClient() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer cli.Close() - containers, err := project.GetActiveContainers(cli) + status, err := deployment.GetStatus(cli) if err != nil { - if err.Error() == project.NoContainersResp { - // This is different from having 2 containers active - - // noContainersResp means that no attempt to build the project - // was made or the project was cleanly shut down. - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, status+project.NoContainersResp) - return - } http.Error(w, err.Error(), http.StatusInternalServerError) return } - // If there are only 2 containers active, that means that a build - // attempt was made but only the daemon and the docker-compose containers - // are active, indicating a build failure. - if len(containers) == 2 { - errorString := status + "It appears that an attempt to start your project was made but the build failed." - http.Error(w, errorString, http.StatusNotFound) + branchStatus := " - Branch: " + status.Branch + "\n" + commitStatus := " - Commit: " + status.CommitHash + "\n" + commitMessage := " - Message: " + status.CommitMessage + "\n" + buildTypeStatus := " - Build Type: " + status.BuildType + "\n" + statusString := inertiaStatus + branchStatus + commitStatus + commitMessage + buildTypeStatus + + // If build container is active, that means that a build + // attempt was made but only the daemon and docker-compose + // are active, indicating a build failure or build-in-progress + if len(status.Containers) == 0 { + if status.BuildContainerActive { + errorString := statusString + msgBuildInProgress + http.Error(w, errorString, http.StatusOK) + } else { + errorString := statusString + msgNoContainersActive + http.Error(w, errorString, http.StatusOK) + } return } - ignore := map[string]bool{ - "/inertia-daemon": true, - "/docker-compose": true, - } - // Only list project containers - activeContainers := "Active containers:" - for _, container := range containers { - if !ignore[container.Names[0]] { - activeContainers += "\n" + container.Image + " (" + container.Names[0] + ")" - } + activeContainers := "Active containers:\n" + for _, container := range status.Containers { + activeContainers += " - " + container + "\n" } + statusString += activeContainers w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusOK) - fmt.Fprint(w, status+activeContainers) + fmt.Fprint(w, statusString) } diff --git a/daemon/inertia/status_test.go b/daemon/inertia/status_test.go new file mode 100644 index 00000000..e2472b38 --- /dev/null +++ b/daemon/inertia/status_test.go @@ -0,0 +1,134 @@ +package main + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + docker "github.com/docker/docker/client" + "github.com/stretchr/testify/assert" + "github.com/ubclaunchpad/inertia/daemon/inertia/project" +) + +func TestStatusHandlerBuildInProgress(t *testing.T) { + defer func() { deployment = nil }() + // Set up condition + deployment = &FakeDeployment{ + GetStatusFunc: func(*docker.Client) (*project.DeploymentStatus, error) { + return &project.DeploymentStatus{ + Branch: "wow", + CommitHash: "abcde", + CommitMessage: "", + Containers: []string{}, + BuildContainerActive: true, + }, nil + }, + } + + // Assmble request + req, err := http.NewRequest("POST", "/status", nil) + assert.Nil(t, err) + + // Record responses + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(statusHandler) + + handler.ServeHTTP(recorder, req) + assert.Equal(t, recorder.Code, http.StatusOK) + assert.Contains(t, recorder.Body.String(), msgBuildInProgress) +} + +func TestStatusHandlerNoContainers(t *testing.T) { + defer func() { deployment = nil }() + // Set up condition + deployment = &FakeDeployment{ + GetStatusFunc: func(*docker.Client) (*project.DeploymentStatus, error) { + return &project.DeploymentStatus{ + Branch: "wow", + CommitHash: "abcde", + CommitMessage: "", + Containers: []string{}, + BuildContainerActive: false, + }, nil + }, + } + + // Assmble request + req, err := http.NewRequest("POST", "/status", nil) + assert.Nil(t, err) + + // Record responses + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(statusHandler) + + handler.ServeHTTP(recorder, req) + assert.Equal(t, recorder.Code, http.StatusOK) + assert.Contains(t, recorder.Body.String(), msgNoContainersActive) +} + +func TestStatusHandlerActiveContainers(t *testing.T) { + defer func() { deployment = nil }() + // Set up condition + deployment = &FakeDeployment{ + GetStatusFunc: func(*docker.Client) (*project.DeploymentStatus, error) { + return &project.DeploymentStatus{ + Branch: "wow", + CommitHash: "abcde", + CommitMessage: "", + Containers: []string{"mycontainer_1", "yourcontainer_2"}, + BuildContainerActive: false, + }, nil + }, + } + + // Assmble request + req, err := http.NewRequest("POST", "/status", nil) + assert.Nil(t, err) + + // Record responses + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(statusHandler) + + handler.ServeHTTP(recorder, req) + assert.Equal(t, recorder.Code, http.StatusOK) + assert.NotContains(t, recorder.Body.String(), msgNoContainersActive) + assert.NotContains(t, recorder.Body.String(), msgBuildInProgress) + assert.Contains(t, recorder.Body.String(), "mycontainer_1") + assert.Contains(t, recorder.Body.String(), "yourcontainer_2") +} + +func TestStatusHandlerStatusError(t *testing.T) { + defer func() { deployment = nil }() + // Set up condition + deployment = &FakeDeployment{ + GetStatusFunc: func(*docker.Client) (*project.DeploymentStatus, error) { + return nil, errors.New("uh oh") + }, + } + + // Assmble request + req, err := http.NewRequest("POST", "/status", nil) + assert.Nil(t, err) + + // Record responses + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(statusHandler) + + handler.ServeHTTP(recorder, req) + assert.Equal(t, recorder.Code, http.StatusInternalServerError) +} + +func TestStatusHandlerNoDeployment(t *testing.T) { + // Assmble request + req, err := http.NewRequest("POST", "/status", nil) + assert.Nil(t, err) + + // Record responses + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(statusHandler) + + handler.ServeHTTP(recorder, req) + assert.Equal(t, recorder.Code, http.StatusNotFound) + assert.Contains(t, recorder.Body.String(), msgNoDeployment) +} diff --git a/daemon/inertia/up.go b/daemon/inertia/up.go index 4bac1a94..80f0d3c9 100644 --- a/daemon/inertia/up.go +++ b/daemon/inertia/up.go @@ -4,19 +4,15 @@ import ( "encoding/json" "io/ioutil" "net/http" - "os" docker "github.com/docker/docker/client" "github.com/ubclaunchpad/inertia/common" "github.com/ubclaunchpad/inertia/daemon/inertia/auth" "github.com/ubclaunchpad/inertia/daemon/inertia/project" - git "gopkg.in/src-d/go-git.v4" ) // upHandler tries to bring the deployment online func upHandler(w http.ResponseWriter, r *http.Request) { - println("UP request received") - // Get github URL from up request body, err := ioutil.ReadAll(r.Body) if err != nil { @@ -37,46 +33,50 @@ func upHandler(w http.ResponseWriter, r *http.Request) { webhookSecret = upReq.Secret // Check for existing git repository, clone if no git repository exists. - err = common.CheckForGit(project.Directory) - if err != nil { - logger.Println("No git repository present.") - err = project.InitializeRepository(gitOpts.RemoteURL, gitOpts.Branch, logger.GetWriter()) + skipUpdate := false + if deployment == nil { + logger.Println("No deployment detected") + common.RemoveContents(project.Directory) + d, err := project.NewDeployment(project.DeploymentConfig{ + ProjectName: upReq.Project, + BuildType: upReq.BuildType, + RemoteURL: gitOpts.RemoteURL, + Branch: gitOpts.Branch, + PemFilePath: auth.DaemonGithubKeyLocation, + }, logger.GetWriter()) if err != nil { logger.Err(err.Error(), http.StatusPreconditionFailed) return } - } + deployment = d - repo, err := git.PlainOpen(project.Directory) - if err != nil { - logger.Err(err.Error(), http.StatusPreconditionFailed) - return + // Project was just pulled! No need to update again. + skipUpdate = true } // Check for matching remotes - err = project.CompareRemotes(repo, gitOpts.RemoteURL) + err = deployment.CompareRemotes(gitOpts.RemoteURL) if err != nil { logger.Err(err.Error(), http.StatusPreconditionFailed) return } - // Update and deploy project - pemFile, err := os.Open(auth.DaemonGithubKeyLocation) - if err != nil { - return - } - auth, err := auth.GetGithubKey(pemFile) - if err != nil { - return - } + // Change deployment parameters if necessary + deployment.SetConfig(project.DeploymentConfig{ + ProjectName: upReq.Project, + Branch: gitOpts.Branch, + }) + + // Deploy project cli, err := docker.NewEnvClient() if err != nil { logger.Err(err.Error(), http.StatusInternalServerError) return } defer cli.Close() - - err = project.Deploy(auth, repo, gitOpts.Branch, upReq.Project, cli, logger.GetWriter()) + err = deployment.Deploy(project.DeployOptions{ + SkipUpdate: skipUpdate, + }, cli, logger.GetWriter()) if err != nil { logger.Err(err.Error(), http.StatusInternalServerError) return diff --git a/daemon/inertia/webhook.go b/daemon/inertia/webhook.go index f889a75d..5cc73867 100644 --- a/daemon/inertia/webhook.go +++ b/daemon/inertia/webhook.go @@ -8,16 +8,14 @@ import ( docker "github.com/docker/docker/client" "github.com/google/go-github/github" "github.com/ubclaunchpad/inertia/common" - "github.com/ubclaunchpad/inertia/daemon/inertia/auth" "github.com/ubclaunchpad/inertia/daemon/inertia/project" - git "gopkg.in/src-d/go-git.v4" ) var webhookSecret = "inertia" // gitHubWebHookHandler writes a response to a request into the given ResponseWriter. func gitHubWebHookHandler(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, common.DaemonOkResp) + fmt.Fprint(w, common.MsgDaemonOK) payload, err := github.ValidatePayload(r, []byte(webhookSecret)) if err != nil { @@ -52,40 +50,21 @@ func processPushEvent(event *github.PushEvent) { // Ignore event if repository not set up yet, otherwise // let deploy() handle the update. - err := common.CheckForGit(project.Directory) - if err != nil { - println("No git repository present - try running 'inertia $REMOTE up'") + if deployment == nil { + println("No deployment detected - try running 'inertia $REMOTE up'") return } // Check for matching remotes - localRepo, err := git.PlainOpen(project.Directory) - if err != nil { - println(err.Error()) - return - } - err = project.CompareRemotes(localRepo, common.GetSSHRemoteURL(repo.GetGitURL())) + err := deployment.CompareRemotes(common.GetSSHRemoteURL(repo.GetGitURL())) if err != nil { println(err.Error()) return } // If branches match, deploy, otherwise ignore the event. - head, err := localRepo.Head() - if err != nil { - println(err.Error()) - return - } - if head.Name().Short() == branch { + if deployment.GetBranch() == branch { println("Event branch matches deployed branch " + branch) - pemFile, err := os.Open(auth.DaemonGithubKeyLocation) - if err != nil { - return - } - auth, err := auth.GetGithubKey(pemFile) - if err != nil { - return - } cli, err := docker.NewEnvClient() if err != nil { println(err.Error()) @@ -93,14 +72,17 @@ func processPushEvent(event *github.PushEvent) { } defer cli.Close() - err = project.Deploy(auth, localRepo, branch, project.ProjectName, cli, os.Stdout) + // Deploy project + err = deployment.Deploy(project.DeployOptions{ + SkipUpdate: false, + }, cli, os.Stdout) if err != nil { println(err.Error()) } } else { println( "Event branch " + branch + " does not match deployed branch " + - head.Name().Short() + " - ignoring event.", + deployment.GetBranch() + " - ignoring event.", ) } } diff --git a/deploy_cmd.go b/deploy_cmd.go index ed515d73..b80fdc98 100644 --- a/deploy_cmd.go +++ b/deploy_cmd.go @@ -29,7 +29,11 @@ var deploymentUpCmd = &cobra.Command{ if err != nil { log.Fatal(err) } - resp, err := deployment.Up(deployment.Project, stream) + buildType, err := cmd.Flags().GetString("type") + if err != nil { + log.Fatal(err) + } + resp, err := deployment.Up(buildType, stream) if err != nil { log.Fatal(err) } @@ -319,6 +323,7 @@ Run 'inertia [REMOTE] init' to gather this information.`, up := &cobra.Command{} *up = *deploymentUpCmd + up.Flags().String("type", "", "Specify a build method for your project") cmd.AddCommand(up) down := &cobra.Command{} diff --git a/init_cmd.go b/init_cmd.go index 912ce24a..802a5784 100644 --- a/init_cmd.go +++ b/init_cmd.go @@ -8,6 +8,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/ubclaunchpad/inertia/client" + "github.com/ubclaunchpad/inertia/common" ) var initCmd = &cobra.Command{ @@ -18,12 +19,27 @@ There must be a local git repository in order for initialization to succeed.`, Run: func(cmd *cobra.Command, args []string) { version := cmd.Parent().Version - givenVersion, _ := cmd.Flags().GetString("version") + givenVersion, err := cmd.Flags().GetString("version") + if err != nil { + log.Fatal(err) + } if givenVersion != version { version = givenVersion } - err := client.InitializeInertiaProject(cmd.Parent().Version) + // Determine best build type for project + buildType := "herokuish" + cwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + if common.CheckForDockerCompose(cwd) { + println("docker-compose project detected") + buildType = "docker-compose" + } + + // Hello world config file! + err = client.InitializeInertiaProject(version, buildType) if err != nil { log.Fatal(err) } @@ -74,5 +90,5 @@ func init() { // Cobra supports local flags which will only run when this command // is called directly, e.g.: // initCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - addCmd.Flags().String("version", "default", "Inertia daemon version") + initCmd.Flags().String("version", Version, "Specify Inertia daemon version to use") } diff --git a/main.go b/main.go index cc434af6..d5551a7b 100644 --- a/main.go +++ b/main.go @@ -14,13 +14,17 @@ var rootCmd = &cobra.Command{ Use: "inertia", Short: "Inertia is a continuous-deployment scaffold", Version: getVersion(), - Long: `Inertia provides a continuous-deployment scaffold for applications. + Long: `Inertia provides a continuous deployment scaffold for applications. + Initialization involves preparing a server to run an application, then activating a daemon which will continuously update the production server with new releases as they become available in the project's repository. One you have set up a remote with 'inertia remote add [REMOTE]', -use 'inertia [REMOTE] --help' to see what you can do with your remote.`, +use 'inertia [REMOTE] --help' to see what you can do with your remote. + +Repository: https://github.com/ubclaunchpad/inertia/ +Issue tracker: https://github.com/ubclaunchpad/inertia/issues`, } func getVersion() string { diff --git a/remote_cmd.go b/remote_cmd.go index 4a5b99e4..f9972460 100644 --- a/remote_cmd.go +++ b/remote_cmd.go @@ -142,8 +142,6 @@ func addRemoteWalkthrough(in io.Reader, name, port, sshPort, currBranch string, return config.Write() } - - // listCmd represents the inertia list command var listCmd = &cobra.Command{ Use: "ls",