Files
play-with-docker/provisioner/windows.go
2017-09-11 18:09:27 -03:00

313 lines
8.7 KiB
Go

package provisioner
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"sort"
"golang.org/x/net/websocket"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/play-with-docker/play-with-docker/docker"
"github.com/play-with-docker/play-with-docker/pwd/types"
"github.com/play-with-docker/play-with-docker/router"
"github.com/play-with-docker/play-with-docker/storage"
)
var asgService *autoscaling.AutoScaling
var ec2Service *ec2.EC2
func init() {
// Create a session to share configuration, and load external configuration.
sess := session.Must(session.NewSession())
//
// // Create the service's client with the session.
asgService = autoscaling.New(sess)
ec2Service = ec2.New(sess)
}
type windows struct {
factory docker.FactoryApi
storage storage.StorageApi
}
type instanceInfo struct {
publicIP string
privateIP string
id string
}
func NewWindowsASG(f docker.FactoryApi, st storage.StorageApi) *windows {
return &windows{factory: f, storage: st}
}
func (d *windows) InstanceNew(session *types.Session, conf types.InstanceConfig) (*types.Instance, error) {
winfo, err := d.getWindowsInstanceInfo(session.Id)
if err != nil {
return nil, err
}
labels := map[string]string{
"io.tutorius.networkid": session.Id,
"io.tutorius.networking.remote.ip": winfo.privateIP,
}
instanceName := fmt.Sprintf("%s_%s", session.Id[:8], winfo.id)
dockerClient, err := d.factory.GetForSession(session.Id)
if err != nil {
d.releaseInstance(winfo.id)
return nil, err
}
if err = dockerClient.ConfigCreate(instanceName, labels, []byte(instanceName)); err != nil {
d.releaseInstance(winfo.id)
return nil, err
}
instance := &types.Instance{}
instance.Name = instanceName
instance.Image = ""
instance.IP = winfo.privateIP
instance.RoutableIP = instance.IP
instance.SessionId = session.Id
instance.WindowsId = winfo.id
instance.Cert = conf.Cert
instance.Key = conf.Key
instance.Type = conf.Type
instance.ServerCert = conf.ServerCert
instance.ServerKey = conf.ServerKey
instance.CACert = conf.CACert
instance.ProxyHost = router.EncodeHost(session.Id, instance.RoutableIP, router.HostOpts{})
instance.SessionHost = session.Host
return instance, nil
}
func (d *windows) InstanceDelete(session *types.Session, instance *types.Instance) error {
dockerClient, err := d.factory.GetForSession(session.Id)
if err != nil {
return err
}
_, err = asgService.DetachInstances(&autoscaling.DetachInstancesInput{
AutoScalingGroupName: aws.String("pwd-windows"),
InstanceIds: []*string{aws.String(instance.WindowsId)},
ShouldDecrementDesiredCapacity: aws.Bool(false),
})
if err != nil {
return err
}
//return error and don't do anything else
if _, err := ec2Service.TerminateInstances(&ec2.TerminateInstancesInput{InstanceIds: []*string{aws.String(instance.WindowsId)}}); err != nil {
return err
}
err = dockerClient.ConfigDelete(instance.Name)
if err != nil {
return err
}
return d.releaseInstance(instance.WindowsId)
}
type execRes struct {
ExitCode int `json:"exit_code"`
Error string `json:"error"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
}
func (d *windows) InstanceExec(instance *types.Instance, cmd []string) (int, error) {
execBody := struct {
Cmd []string `json:"cmd"`
}{Cmd: cmd}
b, err := json.Marshal(execBody)
if err != nil {
return -1, err
}
resp, err := http.Post(fmt.Sprintf("http://%s:222/exec", instance.IP), "application/json", bytes.NewReader(b))
if err != nil {
log.Println(err)
return -1, err
}
if resp.StatusCode != 200 {
log.Printf("Error exec on instance %s. Got %d\n", instance.Name, resp.StatusCode)
return -1, fmt.Errorf("Error exec on instance %s. Got %d\n", instance.Name, resp.StatusCode)
}
var ex execRes
err = json.NewDecoder(resp.Body).Decode(&ex)
if err != nil {
return -1, err
}
return ex.ExitCode, nil
}
func (d *windows) releaseInstance(instanceId string) error {
return d.storage.WindowsInstanceDelete(instanceId)
}
func (d *windows) InstanceResizeTerminal(instance *types.Instance, rows, cols uint) error {
resp, err := http.Post(fmt.Sprintf("http://%s:222/terminals/1/size?cols=%d&rows=%d", instance.IP, cols, rows), "application/json", nil)
if err != nil {
log.Println(err)
return err
}
if resp.StatusCode != 200 {
log.Printf("Error resizing terminal of instance %s. Got %d\n", instance.Name, resp.StatusCode)
return fmt.Errorf("Error resizing terminal got %d\n", resp.StatusCode)
}
return nil
}
func (d *windows) InstanceGetTerminal(instance *types.Instance) (net.Conn, error) {
resp, err := http.Post(fmt.Sprintf("http://%s:222/terminals/1", instance.IP), "application/json", nil)
if err != nil {
log.Printf("Error creating terminal for instance %s. Got %v\n", instance.Name, err)
return nil, err
}
if resp.StatusCode != 200 {
log.Printf("Error creating terminal for instance %s. Got %d\n", instance.Name, resp.StatusCode)
return nil, fmt.Errorf("Creating terminal got %d\n", resp.StatusCode)
}
url := fmt.Sprintf("ws://%s:222/terminals/1", instance.IP)
ws, err := websocket.Dial(url, "", url)
if err != nil {
log.Println(err)
return nil, err
}
return ws, nil
}
func (d *windows) InstanceUploadFromUrl(instance *types.Instance, fileName, dest, u string) error {
log.Printf("Downloading file [%s]\n", u)
resp, err := http.Get(u)
if err != nil {
return fmt.Errorf("Could not download file [%s]. Error: %s\n", u, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("Could not download file [%s]. Status code: %d\n", u, resp.StatusCode)
}
uploadResp, err := http.Post(fmt.Sprintf("http://%s:222/terminals/1/uploads?dest=%s&file_name=%s", instance.IP, url.QueryEscape(dest), url.QueryEscape(fileName)), "", resp.Body)
if err != nil {
return err
}
if uploadResp.StatusCode != 200 {
return fmt.Errorf("Could not upload file [%s]. Status code: %d\n", fileName, uploadResp.StatusCode)
}
return nil
}
func (d *windows) InstanceUploadFromReader(instance *types.Instance, fileName, dest string, reader io.Reader) error {
uploadResp, err := http.Post(fmt.Sprintf("http://%s:222/terminals/1/uploads?dest=%s&file_name=%s", instance.IP, url.QueryEscape(dest), url.QueryEscape(fileName)), "", reader)
if err != nil {
return err
}
if uploadResp.StatusCode != 200 {
return fmt.Errorf("Could not upload file [%s]. Status code: %d\n", fileName, uploadResp.StatusCode)
}
return nil
}
func (d *windows) getWindowsInstanceInfo(sessionId string) (*instanceInfo, error) {
input := &autoscaling.DescribeAutoScalingGroupsInput{
AutoScalingGroupNames: []*string{aws.String("pwd-windows")},
}
out, err := asgService.DescribeAutoScalingGroups(input)
if err != nil {
return nil, err
}
// there should always be one asg
instances := out.AutoScalingGroups[0].Instances
availInstances := make([]string, len(instances))
// reverse order so older instances are first served
sort.Sort(sort.Reverse(sort.StringSlice(availInstances)))
for i, inst := range instances {
if *inst.LifecycleState == "InService" {
availInstances[i] = *inst.InstanceId
}
}
assignedInstances, err := d.storage.WindowsInstanceGetAll()
assignedInstancesIds := []string{}
for _, ai := range assignedInstances {
assignedInstancesIds = append(assignedInstancesIds, ai.Id)
}
if err != nil {
return nil, err
}
avInstanceId := d.pickFreeInstance(sessionId, availInstances, assignedInstancesIds)
if len(avInstanceId) == 0 {
return nil, errors.New("No Windows instance available")
}
iout, err := ec2Service.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIds: []*string{aws.String(avInstanceId)},
})
if err != nil {
// TODO retry x times and free the instance that was picked?
d.releaseInstance(avInstanceId)
return nil, err
}
instance := iout.Reservations[0].Instances[0]
instanceInfo := &instanceInfo{
publicIP: *instance.PublicIpAddress,
privateIP: *instance.PrivateIpAddress,
id: avInstanceId,
}
//TODO check for free instance, ASG capacity and return
return instanceInfo, nil
}
// select free instance and lock it into db.
// additionally check if ASG needs to be resized
func (d *windows) pickFreeInstance(sessionId string, availInstances, assignedInstances []string) string {
for _, av := range availInstances {
found := false
for _, as := range assignedInstances {
if av == as {
found = true
break
}
}
if !found {
err := d.storage.WindowsInstancePut(&types.WindowsInstance{SessionId: sessionId, Id: av})
if err != nil {
// TODO either storage error or instance is already assigned (race condition)
}
return av
}
}
// all availalbe instances are assigned
return ""
}