diff --git a/api/errors.go b/api/errors.go
index 7cd39f445..709bafb92 100644
--- a/api/errors.go
+++ b/api/errors.go
@@ -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")
diff --git a/api/http/client/client.go b/api/http/client/client.go
index 438be12ad..338aed15d 100644
--- a/api/http/client/client.go
+++ b/api/http/client/client.go
@@ -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{}
diff --git a/api/http/handler/azure.go b/api/http/handler/azure.go
new file mode 100644
index 000000000..a372244a1
--- /dev/null
+++ b/api/http/handler/azure.go
@@ -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)
+}
diff --git a/api/http/handler/endpoint.go b/api/http/handler/endpoint.go
index 23c867303..972f766cf 100644
--- a/api/http/handler/endpoint.go
+++ b/api/http/handler/endpoint.go
@@ -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)
}
diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go
index 77c6ce478..70e8f4e8b 100644
--- a/api/http/handler/handler.go
+++ b/api/http/handler/handler.go
@@ -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)
}
diff --git a/api/http/proxy/azure_transport.go b/api/http/proxy/azure_transport.go
new file mode 100644
index 000000000..dccf451b5
--- /dev/null
+++ b/api/http/proxy/azure_transport.go
@@ -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)
+}
diff --git a/api/http/proxy/transport.go b/api/http/proxy/docker_transport.go
similarity index 100%
rename from api/http/proxy/transport.go
rename to api/http/proxy/docker_transport.go
diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go
index 8f952f2dc..70ba4543d 100644
--- a/api/http/proxy/factory.go
+++ b/api/http/proxy/factory.go
@@ -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"
diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go
index 2a9018102..f2a691634 100644
--- a/api/http/proxy/manager.go
+++ b/api/http/proxy/manager.go
@@ -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
}
diff --git a/api/http/proxy/reverse_proxy.go b/api/http/proxy/reverse_proxy.go
index 4862de9a9..47e71b63e 100644
--- a/api/http/proxy/reverse_proxy.go
+++ b/api/http/proxy/reverse_proxy.go
@@ -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 {
diff --git a/api/http/server.go b/api/http/server.go
index 5e85f8efa..c32374e20 100644
--- a/api/http/server.go
+++ b/api/http/server.go
@@ -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,
diff --git a/api/portainer.go b/api/portainer.go
index 1f2caeb9b..413b37d39 100644
--- a/api/portainer.go
+++ b/api/portainer.go
@@ -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
)
diff --git a/app/__module.js b/app/__module.js
index f5ebe25a2..a1c438489 100644
--- a/app/__module.js
+++ b/app/__module.js
@@ -18,6 +18,7 @@ angular.module('portainer', [
'portainer.templates',
'portainer.app',
'portainer.agent',
+ 'portainer.azure',
'portainer.docker',
'extension.storidge',
'rzModule']);
diff --git a/app/azure/_module.js b/app/azure/_module.js
new file mode 100644
index 000000000..0879e9a54
--- /dev/null
+++ b/app/azure/_module.js
@@ -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);
+}]);
diff --git a/app/azure/components/azure-sidebar-content/azure-sidebar-content.js b/app/azure/components/azure-sidebar-content/azure-sidebar-content.js
new file mode 100644
index 000000000..d1f9230f4
--- /dev/null
+++ b/app/azure/components/azure-sidebar-content/azure-sidebar-content.js
@@ -0,0 +1,3 @@
+angular.module('portainer.azure').component('azureSidebarContent', {
+ templateUrl: 'app/azure/components/azure-sidebar-content/azureSidebarContent.html'
+});
diff --git a/app/azure/components/azure-sidebar-content/azureSidebarContent.html b/app/azure/components/azure-sidebar-content/azureSidebarContent.html
new file mode 100644
index 000000000..01986e8e7
--- /dev/null
+++ b/app/azure/components/azure-sidebar-content/azureSidebarContent.html
@@ -0,0 +1,6 @@
+
+
diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html
new file mode 100644
index 000000000..c87f74735
--- /dev/null
+++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js
new file mode 100644
index 000000000..c83550ca5
--- /dev/null
+++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js
@@ -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: '<'
+ }
+});
diff --git a/app/azure/models/container_group.js b/app/azure/models/container_group.js
new file mode 100644
index 000000000..0ba06a690
--- /dev/null
+++ b/app/azure/models/container_group.js
@@ -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
+ }
+ };
+}
diff --git a/app/azure/models/location.js b/app/azure/models/location.js
new file mode 100644
index 000000000..a010776ba
--- /dev/null
+++ b/app/azure/models/location.js
@@ -0,0 +1,6 @@
+function LocationViewModel(data) {
+ this.Id = data.id;
+ this.SubscriptionId = data.subscriptionId;
+ this.DisplayName = data.displayName;
+ this.Name = data.name;
+}
diff --git a/app/azure/models/provider.js b/app/azure/models/provider.js
new file mode 100644
index 000000000..48cca79e9
--- /dev/null
+++ b/app/azure/models/provider.js
@@ -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;
+}
diff --git a/app/azure/models/resource_group.js b/app/azure/models/resource_group.js
new file mode 100644
index 000000000..aa04f1809
--- /dev/null
+++ b/app/azure/models/resource_group.js
@@ -0,0 +1,6 @@
+function ResourceGroupViewModel(data, subscriptionId) {
+ this.Id = data.id;
+ this.SubscriptionId = subscriptionId;
+ this.Name = data.name;
+ this.Location = data.location;
+}
diff --git a/app/azure/models/subscription.js b/app/azure/models/subscription.js
new file mode 100644
index 000000000..2baa0da4d
--- /dev/null
+++ b/app/azure/models/subscription.js
@@ -0,0 +1,4 @@
+function SubscriptionViewModel(data) {
+ this.Id = data.subscriptionId;
+ this.Name = data.displayName;
+}
diff --git a/app/azure/rest/azure.js b/app/azure/rest/azure.js
new file mode 100644
index 000000000..2621c7d50
--- /dev/null
+++ b/app/azure/rest/azure.js
@@ -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;
+}]);
diff --git a/app/azure/rest/container_group.js b/app/azure/rest/container_group.js
new file mode 100644
index 000000000..76ed5c4dc
--- /dev/null
+++ b/app/azure/rest/container_group.js
@@ -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;
+}]);
diff --git a/app/azure/rest/location.js b/app/azure/rest/location.js
new file mode 100644
index 000000000..9516761c6
--- /dev/null
+++ b/app/azure/rest/location.js
@@ -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' } }
+ });
+}]);
diff --git a/app/azure/rest/provider.js b/app/azure/rest/provider.js
new file mode 100644
index 000000000..e1a848182
--- /dev/null
+++ b/app/azure/rest/provider.js
@@ -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' } }
+ });
+}]);
diff --git a/app/azure/rest/resource_group.js b/app/azure/rest/resource_group.js
new file mode 100644
index 000000000..2147682e3
--- /dev/null
+++ b/app/azure/rest/resource_group.js
@@ -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' } }
+ });
+}]);
diff --git a/app/azure/rest/subscription.js b/app/azure/rest/subscription.js
new file mode 100644
index 000000000..5b30974c6
--- /dev/null
+++ b/app/azure/rest/subscription.js
@@ -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' }
+ });
+}]);
diff --git a/app/azure/services/azureService.js b/app/azure/services/azureService.js
new file mode 100644
index 000000000..8d7def765
--- /dev/null
+++ b/app/azure/services/azureService.js
@@ -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;
+}]);
diff --git a/app/azure/services/containerGroupService.js b/app/azure/services/containerGroupService.js
new file mode 100644
index 000000000..96031a587
--- /dev/null
+++ b/app/azure/services/containerGroupService.js
@@ -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;
+}]);
diff --git a/app/azure/services/locationService.js b/app/azure/services/locationService.js
new file mode 100644
index 000000000..547ed93b3
--- /dev/null
+++ b/app/azure/services/locationService.js
@@ -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;
+}]);
diff --git a/app/azure/services/providerService.js b/app/azure/services/providerService.js
new file mode 100644
index 000000000..88451d4f5
--- /dev/null
+++ b/app/azure/services/providerService.js
@@ -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;
+}]);
diff --git a/app/azure/services/resourceGroupService.js b/app/azure/services/resourceGroupService.js
new file mode 100644
index 000000000..1777edea8
--- /dev/null
+++ b/app/azure/services/resourceGroupService.js
@@ -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;
+}]);
diff --git a/app/azure/services/subscriptionService.js b/app/azure/services/subscriptionService.js
new file mode 100644
index 000000000..f468e1c8e
--- /dev/null
+++ b/app/azure/services/subscriptionService.js
@@ -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;
+}]);
diff --git a/app/azure/views/containerinstances/containerInstancesController.js b/app/azure/views/containerinstances/containerInstancesController.js
new file mode 100644
index 000000000..ecf2c40cd
--- /dev/null
+++ b/app/azure/views/containerinstances/containerInstancesController.js
@@ -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();
+}]);
diff --git a/app/azure/views/containerinstances/containerinstances.html b/app/azure/views/containerinstances/containerinstances.html
new file mode 100644
index 000000000..489d6b807
--- /dev/null
+++ b/app/azure/views/containerinstances/containerinstances.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Container instances
+
+
+
diff --git a/app/azure/views/containerinstances/create/createContainerInstanceController.js b/app/azure/views/containerinstances/create/createContainerInstanceController.js
new file mode 100644
index 000000000..b3a7ed173
--- /dev/null
+++ b/app/azure/views/containerinstances/create/createContainerInstanceController.js
@@ -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();
+}]);
diff --git a/app/azure/views/containerinstances/create/createcontainerinstance.html b/app/azure/views/containerinstances/create/createcontainerinstance.html
new file mode 100644
index 000000000..001461341
--- /dev/null
+++ b/app/azure/views/containerinstances/create/createcontainerinstance.html
@@ -0,0 +1,160 @@
+
+
+
+ Container instances > Add container
+
+
+
+
diff --git a/app/azure/views/dashboard/dashboard.html b/app/azure/views/dashboard/dashboard.html
new file mode 100644
index 000000000..5726304c1
--- /dev/null
+++ b/app/azure/views/dashboard/dashboard.html
@@ -0,0 +1,33 @@
+
+
+ Dashboard
+
+
+
diff --git a/app/azure/views/dashboard/dashboardController.js b/app/azure/views/dashboard/dashboardController.js
new file mode 100644
index 000000000..f24ff4e29
--- /dev/null
+++ b/app/azure/views/dashboard/dashboardController.js
@@ -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();
+}]);
diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js
index a0a2f4e60..ad0f261a8 100644
--- a/app/docker/views/containers/edit/containerController.js
+++ b/app/docker/views/containers/edit/containerController.js
@@ -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;
diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js
index 85e15aa4f..9db9f4165 100644
--- a/app/portainer/services/api/endpointService.js
+++ b/app/portainer/services/api/endpointService.js
@@ -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;
}]);
diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js
index d2c2120d6..c5413149c 100644
--- a/app/portainer/services/fileUpload.js
+++ b/app/portainer/services/fileUpload.js
@@ -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 = [];
diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js
index c7322a4c3..f50101a94 100644
--- a/app/portainer/services/stateManager.js
+++ b/app/portainer/services/stateManager.js
@@ -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()
diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js
index 7b53dfe91..ab65f2163 100644
--- a/app/portainer/views/auth/authController.js
+++ b/app/portainer/views/auth/authController.js
@@ -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) {
diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js
index bda06746d..3418f251a 100644
--- a/app/portainer/views/endpoints/create/createEndpointController.js
+++ b/app/portainer/views/endpoints/create/createEndpointController.js
@@ -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});
diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html
index 1ab647c4b..e40976bcb 100644
--- a/app/portainer/views/endpoints/create/createendpoint.html
+++ b/app/portainer/views/endpoints/create/createendpoint.html
@@ -36,6 +36,16 @@
Portainer agent
+
+
+
+
@@ -59,6 +69,28 @@
+
Environment details
@@ -78,35 +110,88 @@
-