Add session templates

This commit is contained in:
Jonathan Leibiusky @xetorthio
2017-06-06 14:48:26 -03:00
parent fe8ea11fc6
commit 6d992b5d02
11 changed files with 535 additions and 3 deletions

1
api.go
View File

@@ -61,6 +61,7 @@ func main() {
r.HandleFunc("/ping", handlers.Ping).Methods("GET") r.HandleFunc("/ping", handlers.Ping).Methods("GET")
corsRouter.HandleFunc("/instances/images", handlers.GetInstanceImages).Methods("GET") corsRouter.HandleFunc("/instances/images", handlers.GetInstanceImages).Methods("GET")
corsRouter.HandleFunc("/sessions/{sessionId}", handlers.GetSession).Methods("GET") corsRouter.HandleFunc("/sessions/{sessionId}", handlers.GetSession).Methods("GET")
corsRouter.HandleFunc("/sessions/{sessionId}/setup", handlers.SessionSetup).Methods("POST")
corsRouter.HandleFunc("/sessions/{sessionId}/instances", handlers.NewInstance).Methods("POST") corsRouter.HandleFunc("/sessions/{sessionId}/instances", handlers.NewInstance).Methods("POST")
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/uploads", handlers.FileUpload).Methods("POST") corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/uploads", handlers.FileUpload).Methods("POST")
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", handlers.DeleteInstance).Methods("DELETE") corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", handlers.DeleteInstance).Methods("DELETE")

View File

@@ -4,22 +4,27 @@ import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"context" "context"
"crypto/tls"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/go-connections/tlsconfig"
) )
const ( const (
@@ -44,6 +49,14 @@ type DockerApi interface {
DisconnectNetwork(containerId, networkId string) error DisconnectNetwork(containerId, networkId string) error
DeleteNetwork(id string) error DeleteNetwork(id string) error
Exec(instanceName string, command []string) (int, error) Exec(instanceName string, command []string) (int, error)
New(ip string, cert, key []byte) (DockerApi, error)
SwarmInit() (*SwarmTokens, error)
SwarmJoin(addr, token string) error
}
type SwarmTokens struct {
Manager string
Worker string
} }
type docker struct { type docker struct {
@@ -396,6 +409,73 @@ func (d *docker) DeleteNetwork(id string) error {
return nil return nil
} }
func (d *docker) New(ip string, cert, key []byte) (DockerApi, error) {
// We check if the client needs to use TLS
var tlsConfig *tls.Config
if len(cert) > 0 && len(key) > 0 {
tlsConfig = tlsconfig.ClientDefault()
tlsConfig.InsecureSkipVerify = true
tlsCert, err := tls.X509KeyPair(cert, key)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{tlsCert}
}
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 1 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext}
if tlsConfig != nil {
transport.TLSClientConfig = tlsConfig
}
cli := &http.Client{
Transport: transport,
}
c, err := client.NewClient(fmt.Sprintf("http://%s:2375", ip), api.DefaultVersion, cli, nil)
if err != nil {
return nil, fmt.Errorf("Could not connect to DinD docker daemon. %s", err)
}
// try to connect up to 5 times and then give up
for i := 0; i < 5; i++ {
_, err := c.Ping(context.Background())
if err != nil {
if client.IsErrConnectionFailed(err) {
// connection has failed, maybe instance is not ready yet, sleep and retry
log.Printf("Connection to [%s] has failed, maybe instance is not ready yet, sleeping and retrying in 1 second. Try #%d\n", fmt.Sprintf("http://%s:2375", ip), i+1)
time.Sleep(time.Second)
continue
}
return nil, err
}
}
return NewDocker(c), nil
}
func (d *docker) SwarmInit() (*SwarmTokens, error) {
req := swarm.InitRequest{AdvertiseAddr: "eth0", ListenAddr: "0.0.0.0:2377"}
_, err := d.c.SwarmInit(context.Background(), req)
if err != nil {
return nil, err
}
swarmInfo, err := d.c.SwarmInspect(context.Background())
if err != nil {
return nil, err
}
return &SwarmTokens{
Worker: swarmInfo.JoinTokens.Worker,
Manager: swarmInfo.JoinTokens.Manager,
}, nil
}
func (d *docker) SwarmJoin(addr, token string) error {
req := swarm.JoinRequest{RemoteAddrs: []string{addr}, JoinToken: token, ListenAddr: "0.0.0.0:2377", AdvertiseAddr: "eth0"}
return d.c.SwarmJoin(context.Background(), req)
}
func NewDocker(c *client.Client) *docker { func NewDocker(c *client.Client) *docker {
return &docker{c: c} return &docker{c: c}
} }

35
handlers/session_setup.go Normal file
View File

@@ -0,0 +1,35 @@
package handlers
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/play-with-docker/play-with-docker/pwd"
)
func SessionSetup(rw http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
sessionId := vars["sessionId"]
body := pwd.SessionSetupConf{}
json.NewDecoder(req.Body).Decode(&body)
s := core.SessionGet(sessionId)
if len(s.Instances) > 0 {
log.Println("Cannot setup a session that contains instances")
rw.WriteHeader(http.StatusConflict)
rw.Write([]byte("Cannot setup a session that contains instances"))
return
}
err := core.SessionSetup(s, body)
if err != nil {
log.Println(err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
}

View File

@@ -14,6 +14,10 @@ type mockDocker struct {
connectNetwork func(container, network, ip string) (string, error) connectNetwork func(container, network, ip string) (string, error)
containerResize func(string, uint, uint) error containerResize func(string, uint, uint) error
createContainer func(opts docker.CreateContainerOpts) (string, error) createContainer func(opts docker.CreateContainerOpts) (string, error)
execAttach func(instanceName string, command []string, out io.Writer) (int, error)
new func(ip string, cert, key []byte) (docker.DockerApi, error)
swarmInit func() (*docker.SwarmTokens, error)
swarmJoin func(addr, token string) error
} }
func (m *mockDocker) CreateNetwork(id string) error { func (m *mockDocker) CreateNetwork(id string) error {
@@ -64,6 +68,9 @@ func (m *mockDocker) CreateContainer(opts docker.CreateContainerOpts) (string, e
return "10.0.0.1", nil return "10.0.0.1", nil
} }
func (m *mockDocker) ExecAttach(instanceName string, command []string, out io.Writer) (int, error) { func (m *mockDocker) ExecAttach(instanceName string, command []string, out io.Writer) (int, error) {
if m.execAttach != nil {
return m.execAttach(instanceName, command, out)
}
return 0, nil return 0, nil
} }
func (m *mockDocker) DisconnectNetwork(containerId, networkId string) error { func (m *mockDocker) DisconnectNetwork(containerId, networkId string) error {
@@ -75,6 +82,24 @@ func (m *mockDocker) DeleteNetwork(id string) error {
func (m *mockDocker) Exec(instanceName string, command []string) (int, error) { func (m *mockDocker) Exec(instanceName string, command []string) (int, error) {
return 0, nil return 0, nil
} }
func (m *mockDocker) New(ip string, cert, key []byte) (docker.DockerApi, error) {
if m.new != nil {
return m.new(ip, cert, key)
}
return nil, nil
}
func (m *mockDocker) SwarmInit() (*docker.SwarmTokens, error) {
if m.swarmInit != nil {
return m.swarmInit()
}
return nil, nil
}
func (m *mockDocker) SwarmJoin(addr, token string) error {
if m.swarmJoin != nil {
return m.swarmJoin(addr, token)
}
return nil
}
type mockConn struct { type mockConn struct {
} }

View File

@@ -46,6 +46,7 @@ type PWDApi interface {
SessionDeployStack(session *Session) error SessionDeployStack(session *Session) error
SessionGet(id string) *Session SessionGet(id string) *Session
SessionLoadAndPrepare() error SessionLoadAndPrepare() error
SessionSetup(session *Session, conf SessionSetupConf) error
InstanceNew(session *Session, conf InstanceConfig) (*Instance, error) InstanceNew(session *Session, conf InstanceConfig) (*Instance, error)
InstanceResizeTerminal(instance *Instance, cols, rows uint) error InstanceResizeTerminal(instance *Instance, cols, rows uint) error

View File

@@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/play-with-docker/play-with-docker/config" "github.com/play-with-docker/play-with-docker/config"
"github.com/play-with-docker/play-with-docker/docker"
"github.com/twinj/uuid" "github.com/twinj/uuid"
) )
@@ -23,6 +24,17 @@ func (s *sessionBuilderWriter) Write(p []byte) (n int, err error) {
return len(p), nil return len(p), nil
} }
type SessionSetupConf struct {
Instances []SessionSetupInstanceConf `json:"instances"`
}
type SessionSetupInstanceConf struct {
Image string `json:"image"`
Hostname string `json:"hostname"`
IsSwarmManager bool `json:"is_swarm_manager"`
IsSwarmWorker bool `json:"is_swarm_worker"`
}
type Session struct { type Session struct {
rw sync.Mutex rw sync.Mutex
Id string `json:"id"` Id string `json:"id"`
@@ -209,6 +221,60 @@ func (p *pwd) SessionLoadAndPrepare() error {
return nil return nil
} }
func (p *pwd) SessionSetup(session *Session, conf SessionSetupConf) error {
var tokens *docker.SwarmTokens = nil
var firstSwarmManager *Instance = nil
// First create all instances and record who is a swarm manager and who is a swarm worker
for _, conf := range conf.Instances {
instanceConf := InstanceConfig{
ImageName: conf.Image,
Hostname: conf.Hostname,
}
i, err := p.InstanceNew(session, instanceConf)
if err != nil {
return err
}
if conf.IsSwarmManager || conf.IsSwarmWorker {
// check if we have connection to the daemon, if not, create it
if i.docker == nil {
dock, err := p.docker.New(i.IP, i.Cert, i.Key)
if err != nil {
return err
}
i.docker = dock
}
}
if conf.IsSwarmManager {
// this is a swarm manager
// if no swarm cluster has been initiated, then initiate it!
if firstSwarmManager == nil {
tkns, err := i.docker.SwarmInit()
if err != nil {
return err
}
tokens = tkns
firstSwarmManager = i
} else {
// cluster has already been initiated, join as manager
err := i.docker.SwarmJoin(fmt.Sprintf("%s:2377", firstSwarmManager.IP), tokens.Manager)
if err != nil {
return err
}
}
}
if conf.IsSwarmWorker {
// this is a swarm worker
err := i.docker.SwarmJoin(fmt.Sprintf("%s:2377", firstSwarmManager.IP), tokens.Worker)
if err != nil {
return err
}
}
}
return nil
}
// This function should be called any time a session needs to be prepared: // This function should be called any time a session needs to be prepared:
// 1. Like when it is created // 1. Like when it is created
// 2. When it was loaded from storage // 2. When it was loaded from storage

View File

@@ -1,10 +1,12 @@
package pwd package pwd
import ( import (
"fmt"
"testing" "testing"
"time" "time"
"github.com/play-with-docker/play-with-docker/config" "github.com/play-with-docker/play-with-docker/config"
"github.com/play-with-docker/play-with-docker/docker"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -80,3 +82,190 @@ func TestSessionNew(t *testing.T) {
assert.Equal(t, expectedSessions, sessions) assert.Equal(t, expectedSessions, sessions)
assert.True(t, saveCalled) assert.True(t, saveCalled)
} }
func TestSessionSetup(t *testing.T) {
ips := []string{"10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4", "10.0.0.5"}
nextIp := 0
swarmInitOnMaster1 := false
manager2JoinedHasManager := false
manager3JoinedHasManager := false
worker1JoinedHasWorker := false
dock := &mockDocker{}
dock.createContainer = func(opts docker.CreateContainerOpts) (string, error) {
ip := ips[nextIp]
nextIp++
return ip, nil
}
dock.new = func(ip string, cert, key []byte) (docker.DockerApi, error) {
if ip == "10.0.0.1" {
return &mockDocker{
swarmInit: func() (*docker.SwarmTokens, error) {
swarmInitOnMaster1 = true
return &docker.SwarmTokens{Worker: "worker-join-token", Manager: "manager-join-token"}, nil
},
}, nil
}
if ip == "10.0.0.2" {
return &mockDocker{
swarmInit: func() (*docker.SwarmTokens, error) {
assert.Fail(t, "Shouldn't have reached here.")
return nil, nil
},
swarmJoin: func(addr, token string) error {
if addr == "10.0.0.1:2377" && token == "manager-join-token" {
manager2JoinedHasManager = true
return nil
}
assert.Fail(t, "Shouldn't have reached here.")
return nil
},
}, nil
}
if ip == "10.0.0.3" {
return &mockDocker{
swarmInit: func() (*docker.SwarmTokens, error) {
assert.Fail(t, "Shouldn't have reached here.")
return nil, nil
},
swarmJoin: func(addr, token string) error {
if addr == "10.0.0.1:2377" && token == "manager-join-token" {
manager3JoinedHasManager = true
return nil
}
assert.Fail(t, "Shouldn't have reached here.")
return nil
},
}, nil
}
if ip == "10.0.0.4" {
return &mockDocker{
swarmInit: func() (*docker.SwarmTokens, error) {
assert.Fail(t, "Shouldn't have reached here.")
return nil, nil
},
swarmJoin: func(addr, token string) error {
if addr == "10.0.0.1:2377" && token == "worker-join-token" {
worker1JoinedHasWorker = true
return nil
}
assert.Fail(t, "Shouldn't have reached here.")
return nil
},
}, nil
}
assert.Fail(t, "Shouldn't have reached here.")
return nil, nil
}
tasks := &mockTasks{}
broadcast := &mockBroadcast{}
storage := &mockStorage{}
p := NewPWD(dock, tasks, broadcast, storage)
s, e := p.SessionNew(time.Hour, "", "")
assert.Nil(t, e)
err := p.SessionSetup(s, SessionSetupConf{
Instances: []SessionSetupInstanceConf{
{
Image: "franela/dind",
IsSwarmManager: true,
Hostname: "manager1",
},
{
IsSwarmManager: true,
Hostname: "manager2",
},
{
Image: "franela/dind:overlay2-dev",
IsSwarmManager: true,
Hostname: "manager3",
},
{
IsSwarmWorker: true,
Hostname: "worker1",
},
{
Hostname: "other",
},
},
})
assert.Nil(t, err)
assert.Equal(t, 5, len(s.Instances))
manager1 := fmt.Sprintf("%s_manager1", s.Id[:8])
manager1Received := *s.Instances[manager1]
assert.Equal(t, Instance{
Name: manager1,
Image: "franela/dind",
Hostname: "manager1",
IP: "10.0.0.1",
Alias: "",
IsDockerHost: true,
session: s,
conn: manager1Received.conn,
docker: manager1Received.docker,
}, manager1Received)
manager2 := fmt.Sprintf("%s_manager2", s.Id[:8])
manager2Received := *s.Instances[manager2]
assert.Equal(t, Instance{
Name: manager2,
Image: "franela/dind",
Hostname: "manager2",
IP: "10.0.0.2",
Alias: "",
IsDockerHost: true,
session: s,
conn: manager2Received.conn,
docker: manager2Received.docker,
}, manager2Received)
manager3 := fmt.Sprintf("%s_manager3", s.Id[:8])
manager3Received := *s.Instances[manager3]
assert.Equal(t, Instance{
Name: manager3,
Image: "franela/dind:overlay2-dev",
Hostname: "manager3",
IP: "10.0.0.3",
Alias: "",
IsDockerHost: true,
session: s,
conn: manager3Received.conn,
docker: manager3Received.docker,
}, manager3Received)
worker1 := fmt.Sprintf("%s_worker1", s.Id[:8])
worker1Received := *s.Instances[worker1]
assert.Equal(t, Instance{
Name: worker1,
Image: "franela/dind",
Hostname: "worker1",
IP: "10.0.0.4",
Alias: "",
IsDockerHost: true,
session: s,
conn: worker1Received.conn,
docker: worker1Received.docker,
}, worker1Received)
other := fmt.Sprintf("%s_other", s.Id[:8])
otherReceived := *s.Instances[other]
assert.Equal(t, Instance{
Name: other,
Image: "franela/dind",
Hostname: "other",
IP: "10.0.0.5",
Alias: "",
IsDockerHost: true,
session: s,
conn: otherReceived.conn,
docker: otherReceived.docker,
}, otherReceived)
assert.True(t, swarmInitOnMaster1)
assert.True(t, manager2JoinedHasManager)
assert.True(t, manager3JoinedHasManager)
assert.True(t, worker1JoinedHasWorker)
}

View File

@@ -19,8 +19,8 @@
} }
} }
app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', '$window', 'TerminalService', 'KeyboardShortcutService', 'InstanceService', function($scope, $log, $http, $location, $timeout, $mdDialog, $window, TerminalService, KeyboardShortcutService, InstanceService) { app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', '$window', 'TerminalService', 'KeyboardShortcutService', 'InstanceService', 'SessionService', function($scope, $log, $http, $location, $timeout, $mdDialog, $window, TerminalService, KeyboardShortcutService, InstanceService, SessionService) {
$scope.sessionId = window.location.pathname.replace('/p/', ''); $scope.sessionId = SessionService.getCurrentSessionId();
$scope.instances = []; $scope.instances = [];
$scope.idx = {}; $scope.idx = {};
$scope.selectedInstance = null; $scope.selectedInstance = null;
@@ -363,7 +363,7 @@
$mdIconProvider.defaultIconSet('../assets/social-icons.svg', 24); $mdIconProvider.defaultIconSet('../assets/social-icons.svg', 24);
}]) }])
.component('settingsIcon', { .component('settingsIcon', {
template : "<md-button ng-click='$ctrl.onClick()'><md-icon class='material-icons'>settings</md-icon></md-button>", template : "<md-button class='md-mini' ng-click='$ctrl.onClick()'><md-icon class='material-icons'>settings</md-icon></md-button>",
controller : function($mdDialog) { controller : function($mdDialog) {
var $ctrl = this; var $ctrl = this;
$ctrl.onClick = function() { $ctrl.onClick = function() {
@@ -376,6 +376,42 @@
} }
} }
}) })
.component('templatesIcon', {
template : "<md-button class='md-mini' ng-click='$ctrl.onClick()'><md-icon class='material-icons'>build</md-icon></md-button>",
controller : function($mdDialog) {
var $ctrl = this;
$ctrl.onClick = function() {
$mdDialog.show({
controller : function() {},
template : "<templates-dialog></templates-dialog>",
parent: angular.element(document.body),
clickOutsideToClose : true
})
}
}
})
.component("templatesDialog", {
templateUrl : "templates-modal.html",
controller : function($mdDialog, $scope, SessionService) {
var $ctrl = this;
$scope.building = false;
$scope.templates = SessionService.getAvailableTemplates();
$ctrl.close = function() {
$mdDialog.cancel();
}
$ctrl.setupSession = function(setup) {
$scope.building = true;
SessionService.setup(setup, function(err) {
$scope.building = false;
if (err) {
$scope.errorMessage = err;
return;
}
$ctrl.close();
});
}
}
})
.component("settingsDialog", { .component("settingsDialog", {
templateUrl : "settings-modal.html", templateUrl : "settings-modal.html",
controller : function($mdDialog, KeyboardShortcutService, $rootScope, InstanceService, TerminalService) { controller : function($mdDialog, KeyboardShortcutService, $rootScope, InstanceService, TerminalService) {
@@ -419,6 +455,58 @@
} }
} }
}) })
.service("SessionService", function($http) {
var templates = [
{
title: '3 Managers and 2 Workers',
icon: '/assets/swarm.png',
setup: {
instances: [
{hostname: 'manager1', is_swarm_manager: true},
{hostname: 'manager2', is_swarm_manager: true},
{hostname: 'manager3', is_swarm_manager: true},
{hostname: 'worker1', is_swarm_worker: true},
{hostname: 'worker2', is_swarm_worker: true}
]
}
},
{
title: '5 Managers and no workers',
icon: '/assets/swarm.png',
setup: {
instances: [
{hostname: 'manager1', is_swarm_manager: true},
{hostname: 'manager2', is_swarm_manager: true},
{hostname: 'manager3', is_swarm_manager: true},
{hostname: 'manager4', is_swarm_manager: true},
{hostname: 'manager5', is_swarm_manager: true}
]
}
}
];
return {
getAvailableTemplates: getAvailableTemplates,
getCurrentSessionId: getCurrentSessionId,
setup: setup,
};
function getCurrentSessionId() {
return window.location.pathname.replace('/p/', '');
}
function getAvailableTemplates() {
return templates;
}
function setup(plan, cb) {
return $http
.post("/sessions/" + getCurrentSessionId() + "/setup", plan)
.then(function(response) {
if (cb) cb();
}, function(response) {
if (cb) cb(response.data);
});
}
})
.service("InstanceService", function($http) { .service("InstanceService", function($http) {
var instanceImages = []; var instanceImages = [];
_prepopulateAvailableImages(); _prepopulateAvailableImages();

View File

@@ -59,3 +59,6 @@ md-input-container .md-errors-spacer {
background-color: rgba(0,0,0,.5); background-color: rgba(0,0,0,.5);
-webkit-box-shadow: 0 0 1px rgba(255,255,255,.5); -webkit-box-shadow: 0 0 1px rgba(255,255,255,.5);
} }
.md-mini {
min-width: 24px;
}

BIN
www/assets/swarm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -47,6 +47,7 @@
<md-button class="md-warn md-raised" ng-click="closeSession()">Close session</md-button> <md-button class="md-warn md-raised" ng-click="closeSession()">Close session</md-button>
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">
<h1 class="md-toolbar-tools">Instances</h1> <h1 class="md-toolbar-tools">Instances</h1>
<templates-icon></templates-icon>
<settings-icon></settings-icon> <settings-icon></settings-icon>
</div> </div>
</md-toolbar> </md-toolbar>
@@ -145,6 +146,49 @@
</div> </div>
<script type="text/ng-template" id="templates-modal.html">
<md-toolbar>
<div class="md-toolbar-tools">
<h2>Templates</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="$ctrl.close()">
<md-icon class="material-icon" aria-label="Close dialog">close</md-icon>
</md-button>
</div>
</md-toolbar>
<md-dialog-content>
<div class="md-dialog-content" style="width:600px;">
<div layout="row" layout-sm="column" layout-align="space-around" ng-if="building">
<md-progress-circular md-mode="indeterminate"></md-progress-circular>
</div>
<div layout="row" ng-if="errorMessage">
<div flex="100" style="margin-top: 20px; text-align:center; font-weight: bold; color: red;">
{{errorMessage}}
</div>
</div>
<md-list flex ng-if="!building">
<md-list-item class="md-3-line" ng-repeat="template in templates" ng-click="$ctrl.setupSession(template.setup)">
<md-card md-theme="default" md-theme-watch>
<md-card-title>
<md-card-title-text>
<span class="md-headline">{{template.title}}</span>
</md-card-title-text>
<md-card-title-media>
<div class="md-media-sm card-media"><img ng-src="{{template.icon}}" style="height: 75px;" class="md-card-image"></div>
</md-card-title-media>
</md-card-title>
</md-card>
</md-list-item>
</md-list>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-click="$ctrl.close()">
Close
</md-button>
</md-dialog-actions>
</script>
<script type="text/ng-template" id="settings-modal.html"> <script type="text/ng-template" id="settings-modal.html">
<md-toolbar> <md-toolbar>
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">