feat(azure): add experimental Azure endpoint support (#1936)

pull/1839/head
Anthony Lapenna 2018-05-28 16:40:33 +02:00 committed by GitHub
parent 415c6ce5e1
commit 9ad9cc5e2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1665 additions and 79 deletions

View File

@ -44,6 +44,11 @@ const (
ErrEndpointAccessDenied = Error("Access denied to endpoint")
)
// Azure environment errors
const (
ErrAzureInvalidCredentials = Error("Invalid Azure credentials")
)
// Endpoint group errors.
const (
ErrEndpointGroupNotFound = Error("Endpoint group not found")

View File

@ -2,15 +2,68 @@ package client
import (
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/portainer/portainer"
)
// HTTPClient represents a client to send HTTP requests.
type HTTPClient struct {
*http.Client
}
// NewHTTPClient is used to build a new HTTPClient.
func NewHTTPClient() *HTTPClient {
return &HTTPClient{
&http.Client{
Timeout: time.Second * 5,
},
}
}
// AzureAuthenticationResponse represents an Azure API authentication response.
type AzureAuthenticationResponse struct {
AccessToken string `json:"access_token"`
ExpiresOn string `json:"expires_on"`
}
// ExecuteAzureAuthenticationRequest is used to execute an authentication request
// against the Azure API. It re-uses the same http.Client.
func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portainer.AzureCredentials) (*AzureAuthenticationResponse, error) {
loginURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", credentials.TenantID)
params := url.Values{
"grant_type": {"client_credentials"},
"client_id": {credentials.ApplicationID},
"client_secret": {credentials.AuthenticationKey},
"resource": {"https://management.azure.com/"},
}
response, err := client.PostForm(loginURL, params)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusOK {
return nil, portainer.ErrAzureInvalidCredentials
}
var token AzureAuthenticationResponse
err = json.NewDecoder(response.Body).Decode(&token)
if err != nil {
return nil, err
}
return &token, nil
}
// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment
// using the specified host and optional TLS configuration.
// It uses a new Http.Client for each operation.
func ExecutePingOperation(host string, tlsConfig *tls.Config) (bool, error) {
transport := &http.Transport{}

102
api/http/handler/azure.go Normal file
View File

@ -0,0 +1,102 @@
package handler
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// AzureHandler represents an HTTP API handler for proxying requests to the Azure API.
type AzureHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
TeamMembershipService portainer.TeamMembershipService
ProxyManager *proxy.Manager
}
// NewAzureHandler returns a new instance of AzureHandler.
func NewAzureHandler(bouncer *security.RequestBouncer) *AzureHandler {
h := &AzureHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.PathPrefix("/{id}/azure").Handler(
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToAzureAPI)))
return h
}
func (handler *AzureHandler) checkEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID) error {
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
if err != nil {
return err
}
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
return err
}
if !security.AuthorizedEndpointAccess(endpoint, group, userID, memberships) {
return portainer.ErrEndpointAccessDenied
}
return nil
}
func (handler *AzureHandler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
parsedID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole {
err = handler.checkEndpointAccess(endpoint, tokenData.ID)
if err != nil && err == portainer.ErrEndpointAccessDenied {
httperror.WriteErrorResponse(w, err, http.StatusForbidden, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(string(endpointID))
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
http.StripPrefix("/"+id+"/azure", proxy).ServeHTTP(w, r)
}

View File

@ -80,6 +80,7 @@ type (
postEndpointPayload struct {
name string
url string
endpointType int
publicURL string
groupID int
useTLS bool
@ -88,6 +89,9 @@ type (
caCert []byte
cert []byte
key []byte
azureApplicationID string
azureTenantID string
azureAuthenticationKey string
}
)
@ -117,9 +121,46 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
return
}
for i := range filteredEndpoints {
filteredEndpoints[i].AzureCredentials = portainer.AzureCredentials{}
}
encodeJSON(w, filteredEndpoints, handler.Logger)
}
func (handler *EndpointHandler) createAzureEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
credentials := portainer.AzureCredentials{
ApplicationID: payload.azureApplicationID,
TenantID: payload.azureTenantID,
AuthenticationKey: payload.azureAuthenticationKey,
}
httpClient := client.NewHTTPClient()
_, err := httpClient.ExecuteAzureAuthenticationRequest(&credentials)
if err != nil {
return nil, err
}
endpoint := &portainer.Endpoint{
Name: payload.name,
URL: payload.url,
Type: portainer.AzureEnvironment,
GroupID: portainer.EndpointGroupID(payload.groupID),
PublicURL: payload.publicURL,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
AzureCredentials: credentials,
}
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return nil, err
}
return endpoint, nil
}
func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.caCert, payload.cert, payload.key, payload.skipTLSClientVerification, payload.skipTLSServerVerification)
if err != nil {
@ -236,6 +277,10 @@ func (handler *EndpointHandler) createUnsecuredEndpoint(payload *postEndpointPay
}
func (handler *EndpointHandler) createEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
if portainer.EndpointType(payload.endpointType) == portainer.AzureEnvironment {
return handler.createAzureEndpoint(payload)
}
if payload.useTLS {
return handler.createTLSSecuredEndpoint(payload)
}
@ -245,11 +290,35 @@ func (handler *EndpointHandler) createEndpoint(payload *postEndpointPayload) (*p
func convertPostEndpointRequestToPayload(r *http.Request) (*postEndpointPayload, error) {
payload := &postEndpointPayload{}
payload.name = r.FormValue("Name")
endpointType := r.FormValue("EndpointType")
if payload.name == "" || endpointType == "" {
return nil, ErrInvalidRequestFormat
}
parsedType, err := strconv.Atoi(endpointType)
if err != nil {
return nil, err
}
payload.url = r.FormValue("URL")
payload.endpointType = parsedType
if portainer.EndpointType(payload.endpointType) != portainer.AzureEnvironment && payload.url == "" {
return nil, ErrInvalidRequestFormat
}
payload.publicURL = r.FormValue("PublicURL")
if payload.name == "" || payload.url == "" {
return nil, ErrInvalidRequestFormat
if portainer.EndpointType(payload.endpointType) == portainer.AzureEnvironment {
payload.azureApplicationID = r.FormValue("AzureApplicationID")
payload.azureTenantID = r.FormValue("AzureTenantID")
payload.azureAuthenticationKey = r.FormValue("AzureAuthenticationKey")
if payload.azureApplicationID == "" || payload.azureTenantID == "" || payload.azureAuthenticationKey == "" {
return nil, ErrInvalidRequestFormat
}
}
rawGroupID := r.FormValue("GroupID")
@ -336,6 +405,8 @@ func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http
return
}
endpoint.AzureCredentials = portainer.AzureCredentials{}
encodeJSON(w, endpoint, handler.Logger)
}

View File

@ -30,6 +30,7 @@ type Handler struct {
SettingsHandler *SettingsHandler
TemplatesHandler *TemplatesHandler
DockerHandler *DockerHandler
AzureHandler *AzureHandler
WebSocketHandler *WebSocketHandler
UploadHandler *UploadHandler
FileHandler *FileHandler
@ -64,6 +65,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api/endpoints", h.StoridgeHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/extensions"):
http.StripPrefix("/api/endpoints", h.ExtensionHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/azure/"):
http.StripPrefix("/api/endpoints", h.AzureHandler).ServeHTTP(w, r)
default:
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
}

View File

@ -0,0 +1,81 @@
package proxy
import (
"net/http"
"strconv"
"sync"
"time"
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/client"
)
type (
azureAPIToken struct {
value string
expirationTime time.Time
}
// AzureTransport represents a transport used when executing HTTP requests
// against the Azure API.
AzureTransport struct {
credentials *portainer.AzureCredentials
client *client.HTTPClient
token *azureAPIToken
mutex sync.Mutex
}
)
// NewAzureTransport returns a pointer to an AzureTransport instance.
func NewAzureTransport(credentials *portainer.AzureCredentials) *AzureTransport {
return &AzureTransport{
credentials: credentials,
client: client.NewHTTPClient(),
}
}
func (transport *AzureTransport) authenticate() error {
token, err := transport.client.ExecuteAzureAuthenticationRequest(transport.credentials)
if err != nil {
return err
}
expiresOn, err := strconv.ParseInt(token.ExpiresOn, 10, 64)
if err != nil {
return err
}
transport.token = &azureAPIToken{
value: token.AccessToken,
expirationTime: time.Unix(expiresOn, 0),
}
return nil
}
func (transport *AzureTransport) retrieveAuthenticationToken() error {
transport.mutex.Lock()
defer transport.mutex.Unlock()
if transport.token == nil {
return transport.authenticate()
}
timeLimit := time.Now().Add(-5 * time.Minute)
if timeLimit.After(transport.token.expirationTime) {
return transport.authenticate()
}
return nil
}
// RoundTrip is the implementation of the Transport interface.
func (transport *AzureTransport) RoundTrip(request *http.Request) (*http.Response, error) {
err := transport.retrieveAuthenticationToken()
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+transport.token.value)
return http.DefaultTransport.RoundTrip(request)
}

View File

@ -10,6 +10,8 @@ import (
"github.com/portainer/portainer/crypto"
)
const azureAPIBaseURL = "https://management.azure.com"
// proxyFactory is a factory to create reverse proxies to Docker endpoints
type proxyFactory struct {
ResourceControlService portainer.ResourceControlService
@ -20,11 +22,23 @@ type proxyFactory struct {
SignatureService portainer.DigitalSignatureService
}
func (factory *proxyFactory) newExtensionHTTPPRoxy(u *url.URL) http.Handler {
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
u.Scheme = "http"
return newSingleHostReverseProxyWithHostHeader(u)
}
func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) {
url, err := url.Parse(azureAPIBaseURL)
if err != nil {
return nil, err
}
proxy := newSingleHostReverseProxyWithHostHeader(url)
proxy.Transport = NewAzureTransport(credentials)
return proxy, nil
}
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool) (http.Handler, error) {
u.Scheme = "https"

View File

@ -44,33 +44,39 @@ func NewManager(parameters *ManagerParams) *Manager {
}
}
// CreateAndRegisterProxy creates a new HTTP reverse proxy and adds it to the registered proxies.
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
var proxy http.Handler
func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration) (http.Handler, error) {
if endpointURL.Scheme == "tcp" {
if tlsConfig.TLS || tlsConfig.TLSSkipVerify {
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false)
}
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false), nil
}
// Assume unix:// scheme
return manager.proxyFactory.newDockerSocketProxy(endpointURL.Path), nil
}
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
return nil, err
}
enableSignature := false
if endpoint.Type == portainer.AgentOnDockerEnvironment {
enableSignature = true
switch endpoint.Type {
case portainer.AgentOnDockerEnvironment:
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true)
case portainer.AzureEnvironment:
return newAzureProxy(&endpoint.AzureCredentials)
default:
return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig)
}
}
if endpointURL.Scheme == "tcp" {
if endpoint.TLSConfig.TLS {
proxy, err = manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, enableSignature)
if err != nil {
return nil, err
}
} else {
proxy = manager.proxyFactory.newDockerHTTPProxy(endpointURL, enableSignature)
}
} else {
// Assume unix:// scheme
proxy = manager.proxyFactory.newDockerSocketProxy(endpointURL.Path)
// CreateAndRegisterProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
proxy, err := manager.createProxy(endpoint)
if err != nil {
return nil, err
}
manager.proxies.Set(string(endpoint.ID), proxy)
@ -99,7 +105,7 @@ func (manager *Manager) CreateAndRegisterExtensionProxy(key, extensionAPIURL str
return nil, err
}
proxy := manager.proxyFactory.newExtensionHTTPPRoxy(extensionURL)
proxy := manager.proxyFactory.newHTTPProxy(extensionURL)
manager.extensionProxies.Set(key, proxy)
return proxy, nil
}

View File

@ -7,7 +7,7 @@ import (
"strings"
)
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
// newSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
// HTTP header, which NewSingleHostReverseProxy deliberately preserves.
func newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {

View File

@ -88,6 +88,11 @@ func (server *Server) Start() error {
dockerHandler.EndpointGroupService = server.EndpointGroupService
dockerHandler.TeamMembershipService = server.TeamMembershipService
dockerHandler.ProxyManager = proxyManager
var azureHandler = handler.NewAzureHandler(requestBouncer)
azureHandler.EndpointService = server.EndpointService
azureHandler.EndpointGroupService = server.EndpointGroupService
azureHandler.TeamMembershipService = server.TeamMembershipService
azureHandler.ProxyManager = proxyManager
var websocketHandler = handler.NewWebSocketHandler()
websocketHandler.EndpointService = server.EndpointService
websocketHandler.SignatureService = server.SignatureService
@ -140,6 +145,7 @@ func (server *Server) Start() error {
StackHandler: stackHandler,
TemplatesHandler: templatesHandler,
DockerHandler: dockerHandler,
AzureHandler: azureHandler,
WebSocketHandler: websocketHandler,
FileHandler: fileHandler,
UploadHandler: uploadHandler,

View File

@ -175,16 +175,17 @@ type (
// Endpoint represents a Docker endpoint with all the info required
// to connect to it.
Endpoint struct {
ID EndpointID `json:"Id"`
Name string `json:"Name"`
Type EndpointType `json:"Type"`
URL string `json:"URL"`
GroupID EndpointGroupID `json:"GroupId"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
Extensions []EndpointExtension `json:"Extensions"`
ID EndpointID `json:"Id"`
Name string `json:"Name"`
Type EndpointType `json:"Type"`
URL string `json:"URL"`
GroupID EndpointGroupID `json:"GroupId"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
// Deprecated fields
// Deprecated in DBVersion == 4
@ -194,6 +195,14 @@ type (
TLSKeyPath string `json:"TLSKey,omitempty"`
}
// AzureCredentials represents the credentials used to connect to an Azure
// environment.
AzureCredentials struct {
ApplicationID string `json:"ApplicationID"`
TenantID string `json:"TenantID"`
AuthenticationKey string `json:"AuthenticationKey"`
}
// EndpointGroupID represents an endpoint group identifier.
EndpointGroupID int
@ -530,4 +539,6 @@ const (
DockerEnvironment
// AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment
AgentOnDockerEnvironment
// AzureEnvironment represents an endpoint connected to an Azure environment
AzureEnvironment
)

View File

@ -18,6 +18,7 @@ angular.module('portainer', [
'portainer.templates',
'portainer.app',
'portainer.agent',
'portainer.azure',
'portainer.docker',
'extension.storidge',
'rzModule']);

48
app/azure/_module.js Normal file
View File

@ -0,0 +1,48 @@
angular.module('portainer.azure', ['portainer.app'])
.config(['$stateRegistryProvider', function ($stateRegistryProvider) {
'use strict';
var azure = {
name: 'azure',
parent: 'root',
abstract: true
};
var containerInstances = {
name: 'azure.containerinstances',
url: '/containerinstances',
views: {
'content@': {
templateUrl: 'app/azure/views/containerinstances/containerinstances.html',
controller: 'AzureContainerInstancesController'
}
}
};
var containerInstanceCreation = {
name: 'azure.containerinstances.new',
url: '/new/',
views: {
'content@': {
templateUrl: 'app/azure/views/containerinstances/create/createcontainerinstance.html',
controller: 'AzureCreateContainerInstanceController'
}
}
};
var dashboard = {
name: 'azure.dashboard',
url: '/dashboard',
views: {
'content@': {
templateUrl: 'app/azure/views/dashboard/dashboard.html',
controller: 'AzureDashboardController'
}
}
};
$stateRegistryProvider.register(azure);
$stateRegistryProvider.register(containerInstances);
$stateRegistryProvider.register(containerInstanceCreation);
$stateRegistryProvider.register(dashboard);
}]);

View File

@ -0,0 +1,3 @@
angular.module('portainer.azure').component('azureSidebarContent', {
templateUrl: 'app/azure/components/azure-sidebar-content/azureSidebarContent.html'
});

View File

@ -0,0 +1,6 @@
<li class="sidebar-list">
<a ui-sref="azure.dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="azure.containerinstances" ui-sref-active="active">Container instances <span class="menu-icon fa fa-server fa-fw"></span></a>
</li>

View File

@ -0,0 +1,104 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="azure.containerinstances.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add container
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover table-filters">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Location')">
Location
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Location' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Location' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
Published Ports
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="azure.containerinstances.container({ id: item.Id })">{{ item.Name | truncate:50 }}</a>
</td>
<td>{{ item.Location }}</td>
<td>
<a ng-if="item.Ports.length > 0" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ item.IPAddress }}:{{ p.port }}" target="_blank">
<i class="fa fa-external-link-alt" aria-hidden="true"></i> :{{ p.port }}
</a>
<span ng-if="item.Ports.length == 0" >-</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No container available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@ -0,0 +1,14 @@
angular.module('portainer.azure').component('containergroupsDatatable', {
templateUrl: 'app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
removeAction: '<'
}
});

View File

@ -0,0 +1,66 @@
function ContainerGroupDefaultModel() {
this.Location = '';
this.OSType = 'Linux';
this.Name = '';
this.Image = '';
this.AllocatePublicIP = true;
this.Ports = [
{
container: 80,
host: 80,
protocol: 'TCP'
}
];
this.CPU = 1;
this.Memory = 1;
}
function ContainerGroupViewModel(data, subscriptionId, resourceGroupName) {
this.Id = data.id;
this.Name = data.name;
this.Location = data.location;
this.IPAddress = data.properties.ipAddress.ip;
this.Ports = data.properties.ipAddress.ports;
}
function CreateContainerGroupRequest(model) {
this.location = model.Location;
var containerPorts = [];
var addressPorts = [];
for (var i = 0; i < model.Ports.length; i++) {
var binding = model.Ports[i];
containerPorts.push({
port: binding.container
});
addressPorts.push({
port: binding.host,
protocol: binding.protocol
});
}
this.properties = {
osType: model.OSType,
containers: [
{
name: model.Name,
properties: {
image: model.Image,
ports: containerPorts,
resources: {
requests: {
cpu: model.CPU,
memoryInGB: model.Memory
}
}
}
}
],
ipAddress: {
type: model.AllocatePublicIP ? 'Public': 'Private',
ports: addressPorts
}
};
}

View File

@ -0,0 +1,6 @@
function LocationViewModel(data) {
this.Id = data.id;
this.SubscriptionId = data.subscriptionId;
this.DisplayName = data.displayName;
this.Name = data.name;
}

View File

@ -0,0 +1,7 @@
function ContainerInstanceProviderViewModel(data) {
this.Id = data.id;
this.Namespace = data.namespace;
var containerGroupType = _.find(data.resourceTypes, { 'resourceType': 'containerGroups' });
this.Locations = containerGroupType.locations;
}

View File

@ -0,0 +1,6 @@
function ResourceGroupViewModel(data, subscriptionId) {
this.Id = data.id;
this.SubscriptionId = subscriptionId;
this.Name = data.name;
this.Location = data.location;
}

View File

@ -0,0 +1,4 @@
function SubscriptionViewModel(data) {
this.Id = data.subscriptionId;
this.Name = data.displayName;
}

17
app/azure/rest/azure.js Normal file
View File

@ -0,0 +1,17 @@
angular.module('portainer.azure')
.factory('Azure', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
function AzureFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
var service = {};
service.delete = function(id, apiVersion) {
var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/azure' + id + '?api-version=' + apiVersion;
return $http({
method: 'DELETE',
url: url
});
};
return service;
}]);

View File

@ -0,0 +1,41 @@
angular.module('portainer.azure')
.factory('ContainerGroup', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
function ContainerGroupFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
var resource = {};
var base = $resource(
API_ENDPOINT_ENDPOINTS +'/:endpointId/azure/subscriptions/:subscriptionId/providers/Microsoft.ContainerInstance/containerGroups',
{
'endpointId': EndpointProvider.endpointID,
'api-version': '2018-04-01'
},
{
query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } }
}
);
var withResourceGroup = $resource(
API_ENDPOINT_ENDPOINTS +'/:endpointId/azure/subscriptions/:subscriptionId/resourceGroups/:resourceGroupName/providers/Microsoft.ContainerInstance/containerGroups/:containerGroupName',
{
'endpointId': EndpointProvider.endpointID,
'api-version': '2018-04-01'
},
{
create: {
method: 'PUT',
params: {
subscriptionId: '@subscriptionId',
resourceGroupName: '@resourceGroupName',
containerGroupName: '@containerGroupName'
}
}
}
);
resource.query = base.query;
resource.create = withResourceGroup.create;
return resource;
}]);

View File

@ -0,0 +1,12 @@
angular.module('portainer.azure')
.factory('Location', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
function LocationFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS +'/:endpointId/azure/subscriptions/:subscriptionId/locations', {
'endpointId': EndpointProvider.endpointID,
'api-version': '2016-06-01'
},
{
query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } }
});
}]);

View File

@ -0,0 +1,12 @@
angular.module('portainer.azure')
.factory('Provider', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
function ProviderFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS +'/:endpointId/azure/subscriptions/:subscriptionId/providers/:providerNamespace', {
'endpointId': EndpointProvider.endpointID,
'api-version': '2018-02-01'
},
{
get: { method: 'GET', params: { subscriptionId: '@subscriptionId', providerNamespace: '@providerNamespace' } }
});
}]);

View File

@ -0,0 +1,12 @@
angular.module('portainer.azure')
.factory('ResourceGroup', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
function ResourceGroupFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS +'/:endpointId/azure/subscriptions/:subscriptionId/resourcegroups', {
'endpointId': EndpointProvider.endpointID,
'api-version': '2018-02-01'
},
{
query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } }
});
}]);

View File

@ -0,0 +1,12 @@
angular.module('portainer.azure')
.factory('Subscription', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
function SubscriptionFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions', {
'endpointId': EndpointProvider.endpointID,
'api-version': '2016-06-01'
},
{
query: { method: 'GET' }
});
}]);

View File

@ -0,0 +1,66 @@
angular.module('portainer.azure')
.factory('AzureService', ['$q', 'Azure', 'SubscriptionService', 'ResourceGroupService', 'ContainerGroupService', 'ProviderService',
function AzureServiceFactory($q, Azure, SubscriptionService, ResourceGroupService, ContainerGroupService, ProviderService) {
'use strict';
var service = {};
service.deleteContainerGroup = function(id) {
return Azure.delete(id, '2018-04-01');
};
service.createContainerGroup = function(model, subscriptionId, resourceGroupName) {
return ContainerGroupService.create(model, subscriptionId, resourceGroupName);
};
service.subscriptions = function() {
return SubscriptionService.subscriptions();
};
service.containerInstanceProvider = function(subscriptions) {
return retrieveResourcesForEachSubscription(subscriptions, ProviderService.containerInstanceProvider);
};
service.resourceGroups = function(subscriptions) {
return retrieveResourcesForEachSubscription(subscriptions, ResourceGroupService.resourceGroups);
};
service.containerGroups = function(subscriptions) {
return retrieveResourcesForEachSubscription(subscriptions, ContainerGroupService.containerGroups);
};
service.aggregate = function(resourcesBySubcription) {
var aggregatedResources = [];
Object.keys(resourcesBySubcription).forEach(function(key, index) {
aggregatedResources = aggregatedResources.concat(resourcesBySubcription[key]);
});
return aggregatedResources;
};
function retrieveResourcesForEachSubscription(subscriptions, resourceQuery) {
var deferred = $q.defer();
var resources = {};
var resourceQueries = [];
for (var i = 0; i < subscriptions.length; i++) {
var subscription = subscriptions[i];
resourceQueries.push(resourceQuery(subscription.Id));
}
$q.all(resourceQueries)
.then(function success(data) {
for (var i = 0; i < data.length; i++) {
var result = data[i];
resources[subscriptions[i].Id] = result;
}
deferred.resolve(resources);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve resources', err: err });
});
return deferred.promise;
}
return service;
}]);

View File

@ -0,0 +1,33 @@
angular.module('portainer.azure')
.factory('ContainerGroupService', ['$q', 'ContainerGroup', function ContainerGroupServiceFactory($q, ContainerGroup) {
'use strict';
var service = {};
service.containerGroups = function(subscriptionId) {
var deferred = $q.defer();
ContainerGroup.query({ subscriptionId: subscriptionId }).$promise
.then(function success(data) {
var containerGroups = data.value.map(function (item) {
return new ContainerGroupViewModel(item);
});
deferred.resolve(containerGroups);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve container groups', err: err });
});
return deferred.promise;
};
service.create = function(model, subscriptionId, resourceGroupName) {
var payload = new CreateContainerGroupRequest(model);
return ContainerGroup.create({
subscriptionId: subscriptionId,
resourceGroupName: resourceGroupName,
containerGroupName: model.Name
}, payload).$promise;
};
return service;
}]);

View File

@ -0,0 +1,24 @@
angular.module('portainer.azure')
.factory('LocationService', ['$q', 'Location', function LocationServiceFactory($q, Location) {
'use strict';
var service = {};
service.locations = function(subscriptionId) {
var deferred = $q.defer();
Location.query({ subscriptionId: subscriptionId }).$promise
.then(function success(data) {
var locations = data.value.map(function (item) {
return new LocationViewModel(item);
});
deferred.resolve(locations);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve locations', err: err });
});
return deferred.promise;
};
return service;
}]);

View File

@ -0,0 +1,22 @@
angular.module('portainer.azure')
.factory('ProviderService', ['$q', 'Provider', function ProviderServiceFactory($q, Provider) {
'use strict';
var service = {};
service.containerInstanceProvider = function(subscriptionId) {
var deferred = $q.defer();
Provider.get({ subscriptionId: subscriptionId, providerNamespace: 'Microsoft.ContainerInstance' }).$promise
.then(function success(data) {
var provider = new ContainerInstanceProviderViewModel(data);
deferred.resolve(provider);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve provider', err: err });
});
return deferred.promise;
};
return service;
}]);

View File

@ -0,0 +1,24 @@
angular.module('portainer.azure')
.factory('ResourceGroupService', ['$q', 'ResourceGroup', function ResourceGroupServiceFactory($q, ResourceGroup) {
'use strict';
var service = {};
service.resourceGroups = function(subscriptionId) {
var deferred = $q.defer();
ResourceGroup.query({ subscriptionId: subscriptionId }).$promise
.then(function success(data) {
var resourceGroups = data.value.map(function (item) {
return new ResourceGroupViewModel(item, subscriptionId);
});
deferred.resolve(resourceGroups);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve resource groups', err: err });
});
return deferred.promise;
};
return service;
}]);

View File

@ -0,0 +1,24 @@
angular.module('portainer.azure')
.factory('SubscriptionService', ['$q', 'Subscription', function SubscriptionServiceFactory($q, Subscription) {
'use strict';
var service = {};
service.subscriptions = function() {
var deferred = $q.defer();
Subscription.query({}).$promise
.then(function success(data) {
var subscriptions = data.value.map(function (item) {
return new SubscriptionViewModel(item);
});
deferred.resolve(subscriptions);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve subscriptions', err: err });
});
return deferred.promise;
};
return service;
}]);

View File

@ -0,0 +1,41 @@
angular.module('portainer.azure')
.controller('AzureContainerInstancesController', ['$scope', '$state', 'AzureService', 'Notifications',
function ($scope, $state, AzureService, Notifications) {
function initView() {
AzureService.subscriptions()
.then(function success(data) {
var subscriptions = data;
return AzureService.containerGroups(subscriptions);
})
.then(function success(data) {
$scope.containerGroups = AzureService.aggregate(data);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to load container groups');
});
}
$scope.deleteAction = function (selectedItems) {
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (item) {
AzureService.deleteContainerGroup(item.Id)
.then(function success() {
Notifications.success('Container group successfully removed', item.Name);
var index = $scope.containerGroups.indexOf(item);
$scope.containerGroups.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove container group');
})
.finally(function final() {
--actionCount;
if (actionCount === 0) {
$state.reload();
}
});
});
};
initView();
}]);

View File

@ -0,0 +1,19 @@
<rd-header>
<rd-header-title title="Container list">
<a data-toggle="tooltip" title="Refresh" ui-sref="azure.containerinstances" ui-sref-opts="{reload: true}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Container instances</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<containergroups-datatable
title="Containers" title-icon="fa-server"
dataset="containerGroups" table-key="containergroups"
order-by="Name" show-text-filter="true"
remove-action="deleteAction"
></containergroups-datatable>
</div>
</div>

View File

@ -0,0 +1,87 @@
angular.module('portainer.azure')
.controller('AzureCreateContainerInstanceController', ['$q', '$scope', '$state', 'AzureService', 'Notifications',
function ($q, $scope, $state, AzureService, Notifications) {
var allResourceGroups = [];
var allProviders = [];
$scope.state = {
actionInProgress: false,
selectedSubscription: null,
selectedResourceGroup: null
};
$scope.changeSubscription = function() {
var selectedSubscription = $scope.state.selectedSubscription;
updateResourceGroupsAndLocations(selectedSubscription, allResourceGroups, allProviders);
};
$scope.addPortBinding = function() {
$scope.model.Ports.push({ host: '', container: '', protocol: 'TCP' });
};
$scope.removePortBinding = function(index) {
$scope.model.Ports.splice(index, 1);
};
$scope.create = function() {
var model = $scope.model;
var subscriptionId = $scope.state.selectedSubscription.Id;
var resourceGroupName = $scope.state.selectedResourceGroup.Name;
$scope.state.actionInProgress = true;
AzureService.createContainerGroup(model, subscriptionId, resourceGroupName)
.then(function success(data) {
Notifications.success('Container successfully created', model.Name);
$state.go('azure.containerinstances');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create container');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
function updateResourceGroupsAndLocations(subscription, resourceGroups, providers) {
$scope.state.selectedResourceGroup = resourceGroups[subscription.Id][0];
$scope.resourceGroups = resourceGroups[subscription.Id];
var currentSubLocations = providers[subscription.Id].Locations;
$scope.model.Location = currentSubLocations[0];
$scope.locations = currentSubLocations;
}
function initView() {
var model = new ContainerGroupDefaultModel();
AzureService.subscriptions()
.then(function success(data) {
var subscriptions = data;
$scope.state.selectedSubscription = subscriptions[0];
$scope.subscriptions = subscriptions;
return $q.all({
resourceGroups: AzureService.resourceGroups(subscriptions),
containerInstancesProviders: AzureService.containerInstanceProvider(subscriptions)
});
})
.then(function success(data) {
var resourceGroups = data.resourceGroups;
allResourceGroups = resourceGroups;
var containerInstancesProviders = data.containerInstancesProviders;
allProviders = containerInstancesProviders;
$scope.model = model;
var selectedSubscription = $scope.state.selectedSubscription;
updateResourceGroupsAndLocations(selectedSubscription, resourceGroups, containerInstancesProviders);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve Azure resources');
});
}
initView();
}]);

View File

@ -0,0 +1,160 @@
<rd-header>
<rd-header-title title="Create container instance"></rd-header-title>
<rd-header-content>
<a ui-sref="azure.containerinstances">Container instances</a> &gt; Add container
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" autocomplete="off">
<div class="col-sm-12 form-section-title">
Azure settings
</div>
<!-- subscription-input -->
<div class="form-group">
<label for="azure_subscription" class="col-sm-1 control-label text-left">Subscription</label>
<div class="col-sm-11">
<select class="form-control" name="azure_subscription" ng-model="state.selectedSubscription" ng-options="subscription.Name for subscription in subscriptions" ng-change="changeSubscription()"></select>
</div>
</div>
<!-- !subscription-input -->
<!-- resourcegroup-input -->
<div class="form-group">
<label for="azure_resourcegroup" class="col-sm-1 control-label text-left">Resource group</label>
<div class="col-sm-11">
<select class="form-control" name="azure_resourcegroup" ng-model="state.selectedResourceGroup" ng-options="resourceGroup.Name for resourceGroup in resourceGroups"></select>
</div>
</div>
<!-- !resourcegroup-input -->
<!-- location-input -->
<div class="form-group">
<label for="azure_location" class="col-sm-1 control-label text-left">Location</label>
<div class="col-sm-11">
<select class="form-control" name="azure_location" ng-model="model.Location" ng-options="location for location in locations"></select>
</div>
</div>
<!-- !location-input -->
<div class="col-sm-12 form-section-title">
Container configuration
</div>
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="model.Name" name="container_name" placeholder="e.g. myContainer">
</div>
</div>
<!-- !name-input -->
<!-- image-input -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="model.Image" name="image_name" placeholder="e.g. nginx:alpine">
</div>
</div>
<!-- !image-input -->
<!-- os-input -->
<div class="form-group">
<label for="container_os" class="col-sm-1 control-label text-left">OS</label>
<div class="col-sm-11">
<select class="form-control" ng-model="model.OSType" name="container_os">
<option value="Linux">Linux</option>
<option value="Windows">Windows</option>
</select>
</div>
</div>
<!-- !os-input -->
<!-- port-mapping -->
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Port mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
</span>
</div>
<!-- port-mapping-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="binding in model.Ports" style="margin-top: 2px;">
<!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="binding.host" placeholder="e.g. 80">
</div>
<!-- !host-port -->
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="binding.container" placeholder="e.g. 80">
</div>
<!-- !container-port -->
<!-- protocol-actions -->
<div class="input-group col-sm-3 input-group-sm">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="binding.protocol" uib-btn-radio="'TCP'">TCP</label>
<label class="btn btn-primary" ng-model="binding.protocol" uib-btn-radio="'UDP'">UDP</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !protocol-actions -->
</div>
</div>
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
<!-- public-ip -->
<div class="form-group">
<div class="col-sm-12">
<label for="public_ip" class="control-label text-left">
Allocate public IP address
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="public_ip" ng-model="model.AllocatePublicIP"><i></i>
</label>
</div>
</div>
<!-- public-ip -->
<div class="col-sm-12 form-section-title">
Container resources
</div>
<!-- cpu-input -->
<div class="form-group">
<label for="container_cpu" class="col-sm-1 control-label text-left">CPU</label>
<div class="col-sm-11">
<input type="number" class="form-control" ng-model="model.CPU" name="container_cpu" placeholder="1">
</div>
</div>
<!-- !cpu-input -->
<!-- memory-input -->
<div class="form-group">
<label for="container_memory" class="col-sm-1 control-label text-left">Memory</label>
<div class="col-sm-11">
<input type="number" class="form-control" ng-model="model.Memory" name="container_memory" placeholder="1">
</div>
</div>
<!-- !memory-input -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress" ng-click="create()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Deploy the container</span>
<span ng-show="state.actionInProgress">Deployment in progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -0,0 +1,33 @@
<rd-header>
<rd-header-title title="Home"></rd-header-title>
<rd-header-content>Dashboard</rd-header-content>
</rd-header>
<div class="row" ng-if="subscriptions">
<div class="col-sm-12 col-md-6">
<a ui-sref="azure.subscriptions">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-th-list"></i>
</div>
<div class="title">{{ subscriptions.length }}</div>
<div class="comment">Subscriptions</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
<div class="col-sm-12 col-md-6" ng-if="resourceGroups">
<a ui-sref="azure.resourceGroups">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-th-list"></i>
</div>
<div class="title">{{ resourceGroups.length }}</div>
<div class="comment">Resource groups</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
</div>

View File

@ -0,0 +1,21 @@
angular.module('portainer.azure')
.controller('AzureDashboardController', ['$scope', 'AzureService', 'Notifications',
function ($scope, AzureService, Notifications) {
function initView() {
AzureService.subscriptions()
.then(function success(data) {
var subscriptions = data;
$scope.subscriptions = subscriptions;
return AzureService.resourceGroups(subscriptions);
})
.then(function success(data) {
$scope.resourceGroups = AzureService.aggregate(data);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to load dashboard data');
});
}
initView();
}]);

View File

@ -144,7 +144,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co
$scope.state.joinNetworkInProgress = false;
});
};
$scope.commit = function () {
var image = $scope.config.Image;
var registry = $scope.config.Registry;

View File

@ -70,7 +70,7 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
service.createLocalEndpoint = function() {
var deferred = $q.defer();
FileUploadService.createEndpoint('local', 'unix:///var/run/docker.sock', '', 1, false)
FileUploadService.createEndpoint('local', 1, 'unix:///var/run/docker.sock', '', 1, false)
.then(function success(response) {
deferred.resolve(response.data);
})
@ -81,10 +81,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
return deferred.promise;
};
service.createRemoteEndpoint = function(name, URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
service.createRemoteEndpoint = function(name, type, URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
var deferred = $q.defer();
FileUploadService.createEndpoint(name, 'tcp://' + URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
FileUploadService.createEndpoint(name, type, 'tcp://' + URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success(response) {
deferred.resolve(response.data);
})
@ -95,5 +95,19 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
return deferred.promise;
};
service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey) {
var deferred = $q.defer();
FileUploadService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey)
.then(function success(response) {
deferred.resolve(response.data);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to connect to Azure', err: err});
});
return deferred.promise;
};
return service;
}]);

View File

@ -42,11 +42,12 @@ angular.module('portainer.app')
});
};
service.createEndpoint = function(name, URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
service.createEndpoint = function(name, type, URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
return Upload.upload({
url: 'api/endpoints',
data: {
Name: name,
EndpointType: type,
URL: URL,
PublicURL: PublicURL,
GroupID: groupID,
@ -61,6 +62,20 @@ angular.module('portainer.app')
});
};
service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey) {
return Upload.upload({
url: 'api/endpoints',
data: {
Name: name,
EndpointType: 3,
AzureApplicationID: applicationId,
AzureTenantID: tenantId,
AzureAuthenticationKey: authenticationKey
},
ignoreLoadingBar: true
});
};
service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) {
var queue = [];

View File

@ -128,6 +128,14 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
if (loading) {
state.loading = true;
}
if (type === 3) {
state.endpoint.mode = { provider: 'AZURE' };
LocalStorage.storeEndpointState(state.endpoint);
deferred.resolve();
return deferred.promise;
}
$q.all({
version: SystemService.version(),
info: SystemService.info()

View File

@ -13,12 +13,7 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
AuthenticationError: ''
};
function setActiveEndpointAndRedirectToDashboard(endpoint) {
var endpointID = EndpointProvider.endpointID();
if (!endpointID) {
EndpointProvider.setEndpointID(endpoint.Id);
}
function redirectToDockerDashboard(endpoint) {
ExtensionManager.initEndpointExtensions(endpoint.Id)
.then(function success(data) {
var extensions = data;
@ -32,12 +27,31 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
});
}
function redirectToAzureDashboard(endpoint) {
StateManager.updateEndpointState(false, endpoint.Type, [])
.then(function success(data) {
$state.go('azure.dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
});
}
function redirectToDashboard(endpoint) {
EndpointProvider.setEndpointID(endpoint.Id);
if (endpoint.Type === 3) {
return redirectToAzureDashboard(endpoint);
}
redirectToDockerDashboard(endpoint);
}
function unauthenticatedFlow() {
EndpointService.endpoints()
.then(function success(data) {
var endpoints = data;
if (endpoints.length > 0) {
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
redirectToDashboard(endpoints[0]);
} else {
$state.go('portainer.init.endpoint');
}
@ -79,7 +93,7 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
var endpoints = data;
var userDetails = Authentication.getUserDetails();
if (endpoints.length > 0) {
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
redirectToDashboard(endpoints[0]);
} else if (endpoints.length === 0 && userDetails.role === 1) {
$state.go('portainer.init.endpoint');
} else if (endpoints.length === 0 && userDetails.role === 2) {

View File

@ -12,7 +12,10 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
URL: '',
PublicURL: '',
GroupId: 1,
SecurityFormData: new EndpointSecurityFormData()
SecurityFormData: new EndpointSecurityFormData(),
AzureApplicationId: '',
AzureTenantId: '',
AzureAuthenticationKey: ''
};
$scope.addDockerEndpoint = function() {
@ -30,7 +33,7 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
addEndpoint(name, URL, publicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile);
addEndpoint(name, 1, URL, publicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile);
};
$scope.addAgentEndpoint = function() {
@ -39,12 +42,38 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
var groupId = $scope.formValues.GroupId;
addEndpoint(name, URL, publicURL, groupId, true, true, true, null, null, null);
addEndpoint(name, 2, URL, publicURL, groupId, true, true, true, null, null, null);
};
function addEndpoint(name, URL, PublicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
$scope.addAzureEndpoint = function() {
var name = $scope.formValues.Name;
var applicationId = $scope.formValues.AzureApplicationId;
var tenantId = $scope.formValues.AzureTenantId;
var authenticationKey = $scope.formValues.AzureAuthenticationKey;
createAzureEndpoint(name, applicationId, tenantId, authenticationKey);
};
function createAzureEndpoint(name, applicationId, tenantId, authenticationKey) {
var endpoint;
$scope.state.actionInProgress = true;
EndpointService.createRemoteEndpoint(name, URL, PublicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey)
.then(function success() {
Notifications.success('Endpoint created', name);
$state.go('portainer.endpoints', {}, {reload: true});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create endpoint');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
function addEndpoint(name, type, URL, PublicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
$scope.state.actionInProgress = true;
EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success() {
Notifications.success('Endpoint created', name);
$state.go('portainer.endpoints', {}, {reload: true});

View File

@ -36,6 +36,16 @@
<p>Portainer agent</p>
</label>
</div>
<div>
<input type="radio" id="azure_endpoint" ng-model="state.EnvironmentType" value="azure">
<label for="azure_endpoint">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
Azure
</div>
<p>Connect to Microsoft Azure</p>
</label>
</div>
</div>
</div>
<div ng-if="state.EnvironmentType === 'docker'">
@ -59,6 +69,28 @@
</span>
</div>
</div>
<div ng-if="state.EnvironmentType === 'azure'">
<div class="col-sm-12 form-section-title" >
Information
</div>
<div class="form-group">
<div class="col-sm-12">
<span class="small">
<p class="text-muted">
<i class="fa fa-flask orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> This feature is experimental.
</p>
<p class="text-primary">
Connect to Microsoft Azure to manage Azure Container Instances (ACI).
</p>
<p class="text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Have a look at <a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal" target="_blank">the Azure documentation</a> to retrieve
the credentials required below.
</p>
</span>
</div>
</div>
</div>
<div class="col-sm-12 form-section-title">
Environment details
</div>
@ -78,35 +110,88 @@
</div>
<!-- !name-input -->
<!-- endpoint-url-input -->
<div class="form-group">
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input ng-if="state.EnvironmentType === 'docker'" type="text" class="form-control" name="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375" required>
<input ng-if="state.EnvironmentType === 'agent'" type="text" class="form-control" name="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:9001 or tasks.portainer_agent:9001" required>
<div ng-if="state.EnvironmentType === 'docker' || state.EnvironmentType === 'agent'">
<div class="form-group">
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input ng-if="state.EnvironmentType === 'docker'" type="text" class="form-control" name="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375" required>
<input ng-if="state.EnvironmentType === 'agent'" type="text" class="form-control" name="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:9001 or tasks.portainer_agent:9001" required>
</div>
</div>
</div>
<div class="form-group" ng-show="endpointCreationForm.endpoint_url.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="endpointCreationForm.endpoint_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<div class="form-group" ng-show="endpointCreationForm.endpoint_url.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="endpointCreationForm.endpoint_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
</div>
<!-- !endpoint-url-input -->
<!-- endpoint-public-url-input -->
<div class="form-group">
<label for="endpoint_public_url" class="col-sm-3 col-lg-2 control-label text-left">
Public IP
<portainer-tooltip position="bottom" message="URL or IP address where exposed containers will be reachable. This field is optional and will default to the endpoint URL."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="endpoint_public_url" ng-model="formValues.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com">
<div ng-if="state.EnvironmentType === 'docker' || state.EnvironmentType === 'agent'">
<div class="form-group">
<label for="endpoint_public_url" class="col-sm-3 col-lg-2 control-label text-left">
Public IP
<portainer-tooltip position="bottom" message="URL or IP address where exposed containers will be reachable. This field is optional and will default to the endpoint URL."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="endpoint_public_url" ng-model="formValues.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com">
</div>
</div>
</div>
<!-- !endpoint-public-url-input -->
<!-- azure-details -->
<div ng-if="state.EnvironmentType === 'azure'">
<!-- applicationId-input -->
<div class="form-group">
<label for="azure_credential_appid" class="col-sm-3 col-lg-2 control-label text-left">Application ID</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="azure_credential_appid" ng-model="formValues.AzureApplicationId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required>
</div>
</div>
<div class="form-group" ng-show="endpointCreationForm.azure_credential_appid.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="endpointCreationForm.azure_credential_appid.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !applicationId-input -->
<!-- tenantId-input -->
<div class="form-group">
<label for="azure_credential_tenantid" class="col-sm-3 col-lg-2 control-label text-left">Tenant ID</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="azure_credential_tenantid" ng-model="formValues.AzureTenantId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required>
</div>
</div>
<div class="form-group" ng-show="endpointCreationForm.azure_credential_tenantid.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="endpointCreationForm.azure_credential_tenantid.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !tenantId-input -->
<!-- authenticationkey-input -->
<div class="form-group">
<label for="azure_credential_authkey" class="col-sm-3 col-lg-2 control-label text-left">Authentication key</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="azure_credential_authkey" ng-model="formValues.AzureAuthenticationKey" placeholder="cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=" required>
</div>
</div>
<div class="form-group" ng-show="endpointCreationForm.azure_credential_authkey.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="endpointCreationForm.azure_credential_authkey.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !authenticationkey-input -->
</div>
<!-- !azure-details -->
<!-- group -->
<div class="form-group">
<label for="endpoint_group" class="col-sm-3 col-lg-2 control-label text-left">
@ -131,6 +216,10 @@
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
</button>
<button ng-if="state.EnvironmentType === 'azure'" type="submit" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !endpointCreationForm.$valid" ng-click="addAzureEndpoint()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
</button>
</div>
</div>
<!-- !actions -->

View File

@ -1,7 +1,7 @@
<div class="page-wrapper">
<!-- simple box -->
<div class="container simple-box">
<div class="col-sm-10 col-sm-offset-1">
<div class="col-sm-12">
<!-- simple box logo -->
<div class="row">
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
@ -55,6 +55,16 @@
<p>Connect to a Portainer agent</p>
</label>
</div>
<div>
<input type="radio" id="azure_endpoint" ng-model="formValues.EndpointType" value="azure">
<label for="azure_endpoint">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
Azure
</div>
<p>Connect to Microsoft Azure</p>
</label>
</div>
</div>
</div>
<!-- !endpoint-type -->
@ -141,6 +151,78 @@
<!-- !actions -->
</div>
<!-- !agent-endpoint -->
<!-- azure-endpoint -->
<div ng-if="formValues.EndpointType === 'azure'">
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group">
<div class="col-sm-12">
<span class="small">
<p class="text-muted">
<i class="fa fa-flask orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> This feature is experimental.
</p>
<p class="text-primary">
Connect to Microsoft Azure to manage Azure Container Instances (ACI).
</p>
<p class="text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Have a look at <a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal" target="_blank">the Azure documentation</a> to retrieve
the credentials required below.
</p>
</span>
</div>
</div>
<div class="col-sm-12 form-section-title">
Environment
</div>
<!-- name-input -->
<div class="form-group">
<label for="endpoint_name" class="col-sm-4 col-lg-3 control-label text-left">Name</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="endpoint_name" ng-model="formValues.Name" placeholder="e.g. azure-01">
</div>
</div>
<!-- !name-input -->
<div class="col-sm-12 form-section-title">
Azure credentials
</div>
<!-- applicationId-input -->
<div class="form-group">
<label for="azure_credential_appid" class="col-sm-4 col-lg-3 control-label text-left">Application ID</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="azure_credential_appid" ng-model="formValues.AzureApplicationId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
</div>
</div>
<!-- !applicationId-input -->
<!-- tenantId-input -->
<div class="form-group">
<label for="azure_credential_tenantid" class="col-sm-4 col-lg-3 control-label text-left">Tenant ID</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="azure_credential_tenantid" ng-model="formValues.AzureTenantId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
</div>
</div>
<!-- !tenantId-input -->
<!-- authenticationkey-input -->
<div class="form-group">
<label for="azure_credential_authkey" class="col-sm-4 col-lg-3 control-label text-left">Authentication key</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="azure_credential_authkey" ng-model="formValues.AzureAuthenticationKey" placeholder="cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=">
</div>
</div>
<!-- !authenticationkey-input -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.Name || !formValues.AzureApplicationId || !formValues.AzureTenantId || !formValues.AzureAuthenticationKey" ng-click="createAzureEndpoint()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress"><i class="fa fa-bolt" aria-hidden="true"></i> Connect</span>
<span ng-show="state.actionInProgress">Connecting...</span>
</button>
</div>
</div>
<!-- !actions -->
</div>
<!-- !azure-endpoint -->
<!-- remote-endpoint -->
<div ng-if="formValues.EndpointType === 'remote'">
<div class="col-sm-12 form-section-title">

View File

@ -22,7 +22,10 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
TLSSKipClientVerify: false,
TLSCACert: null,
TLSCert: null,
TLSKey: null
TLSKey: null,
AzureApplicationId: '',
AzureTenantId: '',
AzureAuthenticationKey: ''
};
$scope.createLocalEndpoint = function() {
@ -52,12 +55,21 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
});
};
$scope.createAzureEndpoint = function() {
var name = $scope.formValues.Name;
var applicationId = $scope.formValues.AzureApplicationId;
var tenantId = $scope.formValues.AzureTenantId;
var authenticationKey = $scope.formValues.AzureAuthenticationKey;
createAzureEndpoint(name, applicationId, tenantId, authenticationKey);
};
$scope.createAgentEndpoint = function() {
var name = $scope.formValues.Name;
var URL = $scope.formValues.URL;
var PublicURL = URL.split(':')[0];
createRemoteEndpoint(name, URL, PublicURL, true, true, true, null, null, null);
createRemoteEndpoint(name, 2, URL, PublicURL, true, true, true, null, null, null);
};
$scope.createRemoteEndpoint = function() {
@ -71,13 +83,34 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
var TLSCertFile = TLSSKipClientVerify ? null : $scope.formValues.TLSCert;
var TLSKeyFile = TLSSKipClientVerify ? null : $scope.formValues.TLSKey;
createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile);
createRemoteEndpoint(name, 1, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile);
};
function createAzureEndpoint(name, applicationId, tenantId, authenticationKey) {
var endpoint;
$scope.state.actionInProgress = true;
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey)
.then(function success(data) {
endpoint = data;
EndpointProvider.setEndpointID(endpoint.Id);
return StateManager.updateEndpointState(false, endpoint.Type, []);
})
.then(function success(data) {
$state.go('azure.dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Azure environment');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
function createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
var endpoint;
$scope.state.actionInProgress = true;
EndpointService.createRemoteEndpoint(name, URL, PublicURL, 1, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success(data) {
endpoint = data;
EndpointProvider.setEndpointID(endpoint.Id);

View File

@ -15,7 +15,9 @@
select-endpoint="switchEndpoint"
></endpoint-selector>
<li class="sidebar-title"><span class="endpoint-name">{{ activeEndpoint.Name }}</span></li>
<docker-sidebar-content
<azure-sidebar-content ng-if="applicationState.endpoint.mode.provider === 'AZURE'">
</azure-sidebar-content>
<docker-sidebar-content ng-if="applicationState.endpoint.mode.provider !== 'AZURE'"
endpoint-api-version="applicationState.endpoint.apiVersion"
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"

View File

@ -5,6 +5,29 @@ function ($q, $scope, $state, EndpointService, GroupService, StateManager, Endpo
$scope.switchEndpoint = function(endpoint) {
EndpointProvider.setEndpointID(endpoint.Id);
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
if (endpoint.Type === 3) {
switchToAzureEndpoint(endpoint);
} else {
switchToDockerEndpoint(endpoint);
}
};
function switchToAzureEndpoint(endpoint) {
StateManager.updateEndpointState(false, endpoint.Type, [])
.then(function success() {
$scope.currentEndpoint = endpoint;
$state.go('azure.dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
var currentEndpoint = $scope.currentEndpoint;
EndpointProvider.setEndpointID(currentEndpoint.Id);
EndpointProvider.setEndpointPublicURL(currentEndpoint.PublicURL);
return StateManager.updateEndpointState(true, currentEndpoint.Type, currentEndpoint.Extensions);
});
}
function switchToDockerEndpoint(endpoint) {
ExtensionManager.initEndpointExtensions(endpoint.Id)
.then(function success(data) {
var extensions = data;
@ -21,7 +44,7 @@ function ($q, $scope, $state, EndpointService, GroupService, StateManager, Endpo
EndpointProvider.setEndpointPublicURL(currentEndpoint.PublicURL);
return StateManager.updateEndpointState(true, currentEndpoint.Type, currentEndpoint.Extensions);
});
};
}
function setActiveEndpoint(endpoints) {
var activeEndpointID = EndpointProvider.endpointID();