package provisioner import ( "bytes" "encoding/json" "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) 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.Tls = conf.Tls 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) 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, OutOfCapacityError } 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 "" }