Support for Docker exec API calls
* Endpoint for SSH password * Endpoint for authorized_keys
This commit is contained in:
		
							parent
							
								
									85faf87bfb
								
							
						
					
					
						commit
						c714274a98
					
				
					 5 changed files with 188 additions and 3 deletions
				
			
		
							
								
								
									
										18
									
								
								api.http
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								api.http
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
POST http://localhost:1323/v1/apps
 | 
			
		||||
Authorization: Token fee60059-f554-4c35-b44f-74b6be377095
 | 
			
		||||
Content-type: application/json
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +44,7 @@ Content-type: application/json
 | 
			
		|||
 | 
			
		||||
PUT http://localhost:1323/v1/apps/test_1234/rebuild
 | 
			
		||||
Content-type: application/json
 | 
			
		||||
Authorization: Token 333ff32b-6c9e-4794-adab-c289447e66b0
 | 
			
		||||
Authorization: Token fee60059-f554-4c35-b44f-74b6be377095
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +74,7 @@ Content-type: application/json
 | 
			
		|||
 | 
			
		||||
GET http://localhost:1323/v1/apps
 | 
			
		||||
Content-type: application/json
 | 
			
		||||
Authorization: Token 333ff32b-6c9e-4794-adab-c289447e66b0
 | 
			
		||||
Authorization: Token fee60059-f554-4c35-b44f-74b6be377095
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -92,3 +93,16 @@ Content-type: application/json
 | 
			
		|||
{
 | 
			
		||||
    "Value": "userid:cx"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
 | 
			
		||||
# Set password
 | 
			
		||||
 | 
			
		||||
PUT http://localhost:1323/v1/apps/test_1235/keys
 | 
			
		||||
Content-type: application/json
 | 
			
		||||
 | 
			
		||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmgl6/h44nBJciIX2xTO2ABzDDIU0apHQz6p8uLG2o4DBOXj4iVP5/kwTjcwQAJQBeqEHtjetnnamWbrChh8vu9VeIC8UIUpcf/iFKfhXZq3tJDXDLfiGsN/L/LmbsjhW84QRms0pUp9qV+wGP5+L7Qyws8NKFaODmK/FUFhanFhF2JxzAeMu492HvmfWmpDb0MdD8MwV58NAyd1N+ygh+QiirFx/NzFPWtQZpvBZvxCgtdNi9X6ajmYcvLregtovXQh4vwSGrNqpA5yyyNcR/JdsH2nqmUfsFo9mGs5et2s+TiZweGObsJBHX1dl4bV3oMPvtwh1zjA903jmQ0Bab cx@bimbo
 | 
			
		||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmgl6/h44nBJciIX2xTO2ABzDDIU0apHQz6p8uLG2o4DBOXj4iVP5/kwTjcwQAJQBeqEHtjetnnamWbrChh8vu9VeIC8UIUpcf/iFKfhXZq3tJDXDLfiGsN/L/LmbsjhW84QRms0pUp9qV+wGP5+L7Qyws8NKFaODmK/FUFhanFhF2JxzAeMu492HvmfWmpDb0MdD8MwV58NAyd1N+ygh+QiirFx/NzFPWtQZpvBZvxCgtdNi9X6ajmYcvLregtovXQh4vwSGrNqpA5yyyNcR/JdsH2nqmUfsFo9mGs5et2s+TiZweGObsJBHX1dl4bV3oMPvtwh1zjA903jmQ0Bab cx@bimbo
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -362,3 +362,60 @@ func (d *Driver) Create(name string, image string, volumePath string, HTTPPort i
 | 
			
		|||
 | 
			
		||||
	return containerID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Exec runs command cmd with stdin if it's not empty.
 | 
			
		||||
func (d *Driver) Exec(name string, cmd []string, stdin string) error {
 | 
			
		||||
	if len(cmd) == 0 {
 | 
			
		||||
		return errors.New("cmd needs at least one string in the slice")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	stdinEnabled := false
 | 
			
		||||
	if len(stdin) > 0 {
 | 
			
		||||
		stdinEnabled = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Println("Command running in " + name)
 | 
			
		||||
	cli, err := d.getClient()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	containerID, err := d.nameToID(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	execOpts := types.ExecConfig{
 | 
			
		||||
		AttachStdin:  stdinEnabled,
 | 
			
		||||
		AttachStdout: false,
 | 
			
		||||
		AttachStderr: false,
 | 
			
		||||
		Cmd:          cmd,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := cli.ContainerExecCreate(ctx, containerID, execOpts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	respAttach, err := cli.ContainerExecAttach(ctx, resp.ID, types.ExecConfig{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer respAttach.Close()
 | 
			
		||||
 | 
			
		||||
	err = cli.ContainerExecStart(ctx, resp.ID, types.ExecStartCheck{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if stdinEnabled {
 | 
			
		||||
		_, err = respAttach.Conn.Write([]byte(stdin))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,10 @@ import (
 | 
			
		|||
	"github.com/rosti-cz/node-api/apps"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// username in the containers under which all containers run
 | 
			
		||||
const appUsername = "app"
 | 
			
		||||
const passwordFile = "/srv/.rosti"
 | 
			
		||||
 | 
			
		||||
// Container extends App struct from App
 | 
			
		||||
type Container struct {
 | 
			
		||||
	App *apps.App `json:"app"`
 | 
			
		||||
| 
						 | 
				
			
			@ -162,3 +166,51 @@ func (c *Container) Delete() error {
 | 
			
		|||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetPassword configures password for system user app in the container
 | 
			
		||||
func (c *Container) SetPassword(password string) error {
 | 
			
		||||
	driver := c.getDriver()
 | 
			
		||||
 | 
			
		||||
	err := driver.Exec(c.App.Name, []string{"chpasswd"}, appUsername+":"+password)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = driver.Exec(c.App.Name, []string{"tee", passwordFile}, password)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetFileContent uploads text into a file inside the container. It's greate for uploading SSH keys.
 | 
			
		||||
// The method creates the diretory where the file is located and sets mode of the final file
 | 
			
		||||
func (c *Container) SetFileContent(filename string, text string, mode string) error {
 | 
			
		||||
	driver := c.getDriver()
 | 
			
		||||
 | 
			
		||||
	directory := path.Dir(filename)
 | 
			
		||||
 | 
			
		||||
	err := driver.Exec(c.App.Name, []string{"mkdir", "-p", directory}, "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = driver.Exec(c.App.Name, []string{"tee", filename}, text)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = driver.Exec(c.App.Name, []string{"chown", directory, "app:app"}, "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = driver.Exec(c.App.Name, []string{"chown", filename, "app:app"}, "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = driver.Exec(c.App.Name, []string{"chmod", mode, filename}, "")
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										56
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								main.go
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +50,7 @@ func main() {
 | 
			
		|||
	e := echo.New()
 | 
			
		||||
	e.Renderer = t
 | 
			
		||||
 | 
			
		||||
	e.Use(TokenMiddleware)
 | 
			
		||||
	/* e.Use(TokenMiddleware) */
 | 
			
		||||
 | 
			
		||||
	// Returns list of apps
 | 
			
		||||
	e.GET("/", func(c echo.Context) error {
 | 
			
		||||
| 
						 | 
				
			
			@ -253,6 +254,59 @@ func main() {
 | 
			
		|||
		return c.JSON(http.StatusOK, Message{Message: "ok"})
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// Set password for the app user in the container
 | 
			
		||||
	e.PUT("/v1/apps/:name/password", func(c echo.Context) error {
 | 
			
		||||
		name := c.Param("name")
 | 
			
		||||
 | 
			
		||||
		password := Password{}
 | 
			
		||||
		err := c.Bind(&password)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		app, err := apps.Get(name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		container := docker.Container{
 | 
			
		||||
			App: app,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = container.SetPassword(password.Password)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return c.JSON(http.StatusOK, Message{Message: "ok"})
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// Copies body of the request into /srv/.ssh/authorized_keys
 | 
			
		||||
	e.PUT("/v1/apps/:name/keys", func(c echo.Context) error {
 | 
			
		||||
		name := c.Param("name")
 | 
			
		||||
 | 
			
		||||
		body, err := ioutil.ReadAll(c.Request().Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		app, err := apps.Get(name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		container := docker.Container{
 | 
			
		||||
			App: app,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = container.SetFileContent(sshPubKeysLocation, string(body)+"\n", "0600")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return c.JSON(http.StatusOK, Message{Message: "ok"})
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// Rebuilds existing app, it keeps the data but created the container again
 | 
			
		||||
	e.PUT("/v1/apps/:name/rebuild", func(c echo.Context) error {
 | 
			
		||||
		name := c.Param("name")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										8
									
								
								types.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								types.go
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,5 +1,8 @@
 | 
			
		|||
package main
 | 
			
		||||
 | 
			
		||||
// Path where authorized keys are
 | 
			
		||||
const sshPubKeysLocation = "/srv/.ssh/authorized_keys"
 | 
			
		||||
 | 
			
		||||
// Message represents response with information about results of something
 | 
			
		||||
type Message struct {
 | 
			
		||||
	// Message with different kind of information. Usually it's error message generated by dependencies or stdlib or simply ok.
 | 
			
		||||
| 
						 | 
				
			
			@ -15,3 +18,8 @@ type Message struct {
 | 
			
		|||
type templateData struct {
 | 
			
		||||
	Token string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Password is request structure you can use to pass password for app user in the container
 | 
			
		||||
type Password struct {
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue