mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
feat(azure): add experimental Azure endpoint support (#1936)
This commit is contained in:
parent
415c6ce5e1
commit
9ad9cc5e2d
52 changed files with 1665 additions and 79 deletions
|
@ -44,6 +44,11 @@ const (
|
||||||
ErrEndpointAccessDenied = Error("Access denied to endpoint")
|
ErrEndpointAccessDenied = Error("Access denied to endpoint")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Azure environment errors
|
||||||
|
const (
|
||||||
|
ErrAzureInvalidCredentials = Error("Invalid Azure credentials")
|
||||||
|
)
|
||||||
|
|
||||||
// Endpoint group errors.
|
// Endpoint group errors.
|
||||||
const (
|
const (
|
||||||
ErrEndpointGroupNotFound = Error("Endpoint group not found")
|
ErrEndpointGroupNotFound = Error("Endpoint group not found")
|
||||||
|
|
|
@ -2,15 +2,68 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/portainer/portainer"
|
"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
|
// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment
|
||||||
// using the specified host and optional TLS configuration.
|
// 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) {
|
func ExecutePingOperation(host string, tlsConfig *tls.Config) (bool, error) {
|
||||||
transport := &http.Transport{}
|
transport := &http.Transport{}
|
||||||
|
|
||||||
|
|
102
api/http/handler/azure.go
Normal file
102
api/http/handler/azure.go
Normal 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)
|
||||||
|
}
|
|
@ -80,6 +80,7 @@ type (
|
||||||
postEndpointPayload struct {
|
postEndpointPayload struct {
|
||||||
name string
|
name string
|
||||||
url string
|
url string
|
||||||
|
endpointType int
|
||||||
publicURL string
|
publicURL string
|
||||||
groupID int
|
groupID int
|
||||||
useTLS bool
|
useTLS bool
|
||||||
|
@ -88,6 +89,9 @@ type (
|
||||||
caCert []byte
|
caCert []byte
|
||||||
cert []byte
|
cert []byte
|
||||||
key []byte
|
key []byte
|
||||||
|
azureApplicationID string
|
||||||
|
azureTenantID string
|
||||||
|
azureAuthenticationKey string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -117,9 +121,46 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range filteredEndpoints {
|
||||||
|
filteredEndpoints[i].AzureCredentials = portainer.AzureCredentials{}
|
||||||
|
}
|
||||||
|
|
||||||
encodeJSON(w, filteredEndpoints, handler.Logger)
|
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) {
|
func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
|
||||||
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.caCert, payload.cert, payload.key, payload.skipTLSClientVerification, payload.skipTLSServerVerification)
|
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.caCert, payload.cert, payload.key, payload.skipTLSClientVerification, payload.skipTLSServerVerification)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -236,6 +277,10 @@ func (handler *EndpointHandler) createUnsecuredEndpoint(payload *postEndpointPay
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *EndpointHandler) createEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
|
func (handler *EndpointHandler) createEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
|
||||||
|
if portainer.EndpointType(payload.endpointType) == portainer.AzureEnvironment {
|
||||||
|
return handler.createAzureEndpoint(payload)
|
||||||
|
}
|
||||||
|
|
||||||
if payload.useTLS {
|
if payload.useTLS {
|
||||||
return handler.createTLSSecuredEndpoint(payload)
|
return handler.createTLSSecuredEndpoint(payload)
|
||||||
}
|
}
|
||||||
|
@ -245,11 +290,35 @@ func (handler *EndpointHandler) createEndpoint(payload *postEndpointPayload) (*p
|
||||||
func convertPostEndpointRequestToPayload(r *http.Request) (*postEndpointPayload, error) {
|
func convertPostEndpointRequestToPayload(r *http.Request) (*postEndpointPayload, error) {
|
||||||
payload := &postEndpointPayload{}
|
payload := &postEndpointPayload{}
|
||||||
payload.name = r.FormValue("Name")
|
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.url = r.FormValue("URL")
|
||||||
|
payload.endpointType = parsedType
|
||||||
|
|
||||||
|
if portainer.EndpointType(payload.endpointType) != portainer.AzureEnvironment && payload.url == "" {
|
||||||
|
return nil, ErrInvalidRequestFormat
|
||||||
|
}
|
||||||
|
|
||||||
payload.publicURL = r.FormValue("PublicURL")
|
payload.publicURL = r.FormValue("PublicURL")
|
||||||
|
|
||||||
if payload.name == "" || payload.url == "" {
|
if portainer.EndpointType(payload.endpointType) == portainer.AzureEnvironment {
|
||||||
return nil, ErrInvalidRequestFormat
|
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")
|
rawGroupID := r.FormValue("GroupID")
|
||||||
|
@ -336,6 +405,8 @@ func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endpoint.AzureCredentials = portainer.AzureCredentials{}
|
||||||
|
|
||||||
encodeJSON(w, endpoint, handler.Logger)
|
encodeJSON(w, endpoint, handler.Logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ type Handler struct {
|
||||||
SettingsHandler *SettingsHandler
|
SettingsHandler *SettingsHandler
|
||||||
TemplatesHandler *TemplatesHandler
|
TemplatesHandler *TemplatesHandler
|
||||||
DockerHandler *DockerHandler
|
DockerHandler *DockerHandler
|
||||||
|
AzureHandler *AzureHandler
|
||||||
WebSocketHandler *WebSocketHandler
|
WebSocketHandler *WebSocketHandler
|
||||||
UploadHandler *UploadHandler
|
UploadHandler *UploadHandler
|
||||||
FileHandler *FileHandler
|
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)
|
http.StripPrefix("/api/endpoints", h.StoridgeHandler).ServeHTTP(w, r)
|
||||||
case strings.Contains(r.URL.Path, "/extensions"):
|
case strings.Contains(r.URL.Path, "/extensions"):
|
||||||
http.StripPrefix("/api/endpoints", h.ExtensionHandler).ServeHTTP(w, r)
|
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:
|
default:
|
||||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
81
api/http/proxy/azure_transport.go
Normal file
81
api/http/proxy/azure_transport.go
Normal 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)
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"github.com/portainer/portainer/crypto"
|
"github.com/portainer/portainer/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const azureAPIBaseURL = "https://management.azure.com"
|
||||||
|
|
||||||
// proxyFactory is a factory to create reverse proxies to Docker endpoints
|
// proxyFactory is a factory to create reverse proxies to Docker endpoints
|
||||||
type proxyFactory struct {
|
type proxyFactory struct {
|
||||||
ResourceControlService portainer.ResourceControlService
|
ResourceControlService portainer.ResourceControlService
|
||||||
|
@ -20,11 +22,23 @@ type proxyFactory struct {
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newExtensionHTTPPRoxy(u *url.URL) http.Handler {
|
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||||
u.Scheme = "http"
|
u.Scheme = "http"
|
||||||
return newSingleHostReverseProxyWithHostHeader(u)
|
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) {
|
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool) (http.Handler, error) {
|
||||||
u.Scheme = "https"
|
u.Scheme = "https"
|
||||||
|
|
||||||
|
|
|
@ -44,33 +44,39 @@ func NewManager(parameters *ManagerParams) *Manager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAndRegisterProxy creates a new HTTP reverse proxy and adds it to the registered proxies.
|
func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration) (http.Handler, error) {
|
||||||
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
if endpointURL.Scheme == "tcp" {
|
||||||
func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
if tlsConfig.TLS || tlsConfig.TLSSkipVerify {
|
||||||
var proxy http.Handler
|
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)
|
endpointURL, err := url.Parse(endpoint.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
enableSignature := false
|
switch endpoint.Type {
|
||||||
if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
case portainer.AgentOnDockerEnvironment:
|
||||||
enableSignature = true
|
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" {
|
// CreateAndRegisterProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
||||||
if endpoint.TLSConfig.TLS {
|
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
||||||
proxy, err = manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, enableSignature)
|
func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
if err != nil {
|
proxy, err := manager.createProxy(endpoint)
|
||||||
return nil, err
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
} else {
|
|
||||||
proxy = manager.proxyFactory.newDockerHTTPProxy(endpointURL, enableSignature)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Assume unix:// scheme
|
|
||||||
proxy = manager.proxyFactory.newDockerSocketProxy(endpointURL.Path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.proxies.Set(string(endpoint.ID), proxy)
|
manager.proxies.Set(string(endpoint.ID), proxy)
|
||||||
|
@ -99,7 +105,7 @@ func (manager *Manager) CreateAndRegisterExtensionProxy(key, extensionAPIURL str
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := manager.proxyFactory.newExtensionHTTPPRoxy(extensionURL)
|
proxy := manager.proxyFactory.newHTTPProxy(extensionURL)
|
||||||
manager.extensionProxies.Set(key, proxy)
|
manager.extensionProxies.Set(key, proxy)
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"strings"
|
"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
|
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves.
|
// HTTP header, which NewSingleHostReverseProxy deliberately preserves.
|
||||||
func newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
func newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||||
|
|
|
@ -88,6 +88,11 @@ func (server *Server) Start() error {
|
||||||
dockerHandler.EndpointGroupService = server.EndpointGroupService
|
dockerHandler.EndpointGroupService = server.EndpointGroupService
|
||||||
dockerHandler.TeamMembershipService = server.TeamMembershipService
|
dockerHandler.TeamMembershipService = server.TeamMembershipService
|
||||||
dockerHandler.ProxyManager = proxyManager
|
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()
|
var websocketHandler = handler.NewWebSocketHandler()
|
||||||
websocketHandler.EndpointService = server.EndpointService
|
websocketHandler.EndpointService = server.EndpointService
|
||||||
websocketHandler.SignatureService = server.SignatureService
|
websocketHandler.SignatureService = server.SignatureService
|
||||||
|
@ -140,6 +145,7 @@ func (server *Server) Start() error {
|
||||||
StackHandler: stackHandler,
|
StackHandler: stackHandler,
|
||||||
TemplatesHandler: templatesHandler,
|
TemplatesHandler: templatesHandler,
|
||||||
DockerHandler: dockerHandler,
|
DockerHandler: dockerHandler,
|
||||||
|
AzureHandler: azureHandler,
|
||||||
WebSocketHandler: websocketHandler,
|
WebSocketHandler: websocketHandler,
|
||||||
FileHandler: fileHandler,
|
FileHandler: fileHandler,
|
||||||
UploadHandler: uploadHandler,
|
UploadHandler: uploadHandler,
|
||||||
|
|
|
@ -175,16 +175,17 @@ type (
|
||||||
// Endpoint represents a Docker endpoint with all the info required
|
// Endpoint represents a Docker endpoint with all the info required
|
||||||
// to connect to it.
|
// to connect to it.
|
||||||
Endpoint struct {
|
Endpoint struct {
|
||||||
ID EndpointID `json:"Id"`
|
ID EndpointID `json:"Id"`
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Type EndpointType `json:"Type"`
|
Type EndpointType `json:"Type"`
|
||||||
URL string `json:"URL"`
|
URL string `json:"URL"`
|
||||||
GroupID EndpointGroupID `json:"GroupId"`
|
GroupID EndpointGroupID `json:"GroupId"`
|
||||||
PublicURL string `json:"PublicURL"`
|
PublicURL string `json:"PublicURL"`
|
||||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||||
Extensions []EndpointExtension `json:"Extensions"`
|
Extensions []EndpointExtension `json:"Extensions"`
|
||||||
|
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
|
@ -194,6 +195,14 @@ type (
|
||||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
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 represents an endpoint group identifier.
|
||||||
EndpointGroupID int
|
EndpointGroupID int
|
||||||
|
|
||||||
|
@ -530,4 +539,6 @@ const (
|
||||||
DockerEnvironment
|
DockerEnvironment
|
||||||
// AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment
|
// AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment
|
||||||
AgentOnDockerEnvironment
|
AgentOnDockerEnvironment
|
||||||
|
// AzureEnvironment represents an endpoint connected to an Azure environment
|
||||||
|
AzureEnvironment
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,6 +18,7 @@ angular.module('portainer', [
|
||||||
'portainer.templates',
|
'portainer.templates',
|
||||||
'portainer.app',
|
'portainer.app',
|
||||||
'portainer.agent',
|
'portainer.agent',
|
||||||
|
'portainer.azure',
|
||||||
'portainer.docker',
|
'portainer.docker',
|
||||||
'extension.storidge',
|
'extension.storidge',
|
||||||
'rzModule']);
|
'rzModule']);
|
||||||
|
|
48
app/azure/_module.js
Normal file
48
app/azure/_module.js
Normal 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);
|
||||||
|
}]);
|
|
@ -0,0 +1,3 @@
|
||||||
|
angular.module('portainer.azure').component('azureSidebarContent', {
|
||||||
|
templateUrl: 'app/azure/components/azure-sidebar-content/azureSidebarContent.html'
|
||||||
|
});
|
|
@ -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>
|
|
@ -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>
|
|
@ -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: '<'
|
||||||
|
}
|
||||||
|
});
|
66
app/azure/models/container_group.js
Normal file
66
app/azure/models/container_group.js
Normal 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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
6
app/azure/models/location.js
Normal file
6
app/azure/models/location.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
function LocationViewModel(data) {
|
||||||
|
this.Id = data.id;
|
||||||
|
this.SubscriptionId = data.subscriptionId;
|
||||||
|
this.DisplayName = data.displayName;
|
||||||
|
this.Name = data.name;
|
||||||
|
}
|
7
app/azure/models/provider.js
Normal file
7
app/azure/models/provider.js
Normal 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;
|
||||||
|
}
|
6
app/azure/models/resource_group.js
Normal file
6
app/azure/models/resource_group.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
function ResourceGroupViewModel(data, subscriptionId) {
|
||||||
|
this.Id = data.id;
|
||||||
|
this.SubscriptionId = subscriptionId;
|
||||||
|
this.Name = data.name;
|
||||||
|
this.Location = data.location;
|
||||||
|
}
|
4
app/azure/models/subscription.js
Normal file
4
app/azure/models/subscription.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
function SubscriptionViewModel(data) {
|
||||||
|
this.Id = data.subscriptionId;
|
||||||
|
this.Name = data.displayName;
|
||||||
|
}
|
17
app/azure/rest/azure.js
Normal file
17
app/azure/rest/azure.js
Normal 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;
|
||||||
|
}]);
|
41
app/azure/rest/container_group.js
Normal file
41
app/azure/rest/container_group.js
Normal 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;
|
||||||
|
}]);
|
12
app/azure/rest/location.js
Normal file
12
app/azure/rest/location.js
Normal 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' } }
|
||||||
|
});
|
||||||
|
}]);
|
12
app/azure/rest/provider.js
Normal file
12
app/azure/rest/provider.js
Normal 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' } }
|
||||||
|
});
|
||||||
|
}]);
|
12
app/azure/rest/resource_group.js
Normal file
12
app/azure/rest/resource_group.js
Normal 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' } }
|
||||||
|
});
|
||||||
|
}]);
|
12
app/azure/rest/subscription.js
Normal file
12
app/azure/rest/subscription.js
Normal 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' }
|
||||||
|
});
|
||||||
|
}]);
|
66
app/azure/services/azureService.js
Normal file
66
app/azure/services/azureService.js
Normal 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;
|
||||||
|
}]);
|
33
app/azure/services/containerGroupService.js
Normal file
33
app/azure/services/containerGroupService.js
Normal 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;
|
||||||
|
}]);
|
24
app/azure/services/locationService.js
Normal file
24
app/azure/services/locationService.js
Normal 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;
|
||||||
|
}]);
|
22
app/azure/services/providerService.js
Normal file
22
app/azure/services/providerService.js
Normal 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;
|
||||||
|
}]);
|
24
app/azure/services/resourceGroupService.js
Normal file
24
app/azure/services/resourceGroupService.js
Normal 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;
|
||||||
|
}]);
|
24
app/azure/services/subscriptionService.js
Normal file
24
app/azure/services/subscriptionService.js
Normal 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;
|
||||||
|
}]);
|
|
@ -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();
|
||||||
|
}]);
|
19
app/azure/views/containerinstances/containerinstances.html
Normal file
19
app/azure/views/containerinstances/containerinstances.html
Normal 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>
|
|
@ -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();
|
||||||
|
}]);
|
|
@ -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> > 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>
|
33
app/azure/views/dashboard/dashboard.html
Normal file
33
app/azure/views/dashboard/dashboard.html
Normal 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>
|
21
app/azure/views/dashboard/dashboardController.js
Normal file
21
app/azure/views/dashboard/dashboardController.js
Normal 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();
|
||||||
|
}]);
|
|
@ -144,7 +144,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co
|
||||||
$scope.state.joinNetworkInProgress = false;
|
$scope.state.joinNetworkInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.commit = function () {
|
$scope.commit = function () {
|
||||||
var image = $scope.config.Image;
|
var image = $scope.config.Image;
|
||||||
var registry = $scope.config.Registry;
|
var registry = $scope.config.Registry;
|
||||||
|
|
|
@ -70,7 +70,7 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
service.createLocalEndpoint = function() {
|
service.createLocalEndpoint = function() {
|
||||||
var deferred = $q.defer();
|
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) {
|
.then(function success(response) {
|
||||||
deferred.resolve(response.data);
|
deferred.resolve(response.data);
|
||||||
})
|
})
|
||||||
|
@ -81,10 +81,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
return deferred.promise;
|
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();
|
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) {
|
.then(function success(response) {
|
||||||
deferred.resolve(response.data);
|
deferred.resolve(response.data);
|
||||||
})
|
})
|
||||||
|
@ -95,5 +95,19 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
return deferred.promise;
|
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;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -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({
|
return Upload.upload({
|
||||||
url: 'api/endpoints',
|
url: 'api/endpoints',
|
||||||
data: {
|
data: {
|
||||||
Name: name,
|
Name: name,
|
||||||
|
EndpointType: type,
|
||||||
URL: URL,
|
URL: URL,
|
||||||
PublicURL: PublicURL,
|
PublicURL: PublicURL,
|
||||||
GroupID: groupID,
|
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) {
|
service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||||
var queue = [];
|
var queue = [];
|
||||||
|
|
||||||
|
|
|
@ -128,6 +128,14 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
||||||
if (loading) {
|
if (loading) {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 3) {
|
||||||
|
state.endpoint.mode = { provider: 'AZURE' };
|
||||||
|
LocalStorage.storeEndpointState(state.endpoint);
|
||||||
|
deferred.resolve();
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
version: SystemService.version(),
|
version: SystemService.version(),
|
||||||
info: SystemService.info()
|
info: SystemService.info()
|
||||||
|
|
|
@ -13,12 +13,7 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
|
||||||
AuthenticationError: ''
|
AuthenticationError: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
function setActiveEndpointAndRedirectToDashboard(endpoint) {
|
function redirectToDockerDashboard(endpoint) {
|
||||||
var endpointID = EndpointProvider.endpointID();
|
|
||||||
if (!endpointID) {
|
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtensionManager.initEndpointExtensions(endpoint.Id)
|
ExtensionManager.initEndpointExtensions(endpoint.Id)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var extensions = 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() {
|
function unauthenticatedFlow() {
|
||||||
EndpointService.endpoints()
|
EndpointService.endpoints()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpoints = data;
|
var endpoints = data;
|
||||||
if (endpoints.length > 0) {
|
if (endpoints.length > 0) {
|
||||||
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
redirectToDashboard(endpoints[0]);
|
||||||
} else {
|
} else {
|
||||||
$state.go('portainer.init.endpoint');
|
$state.go('portainer.init.endpoint');
|
||||||
}
|
}
|
||||||
|
@ -79,7 +93,7 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
|
||||||
var endpoints = data;
|
var endpoints = data;
|
||||||
var userDetails = Authentication.getUserDetails();
|
var userDetails = Authentication.getUserDetails();
|
||||||
if (endpoints.length > 0) {
|
if (endpoints.length > 0) {
|
||||||
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
redirectToDashboard(endpoints[0]);
|
||||||
} else if (endpoints.length === 0 && userDetails.role === 1) {
|
} else if (endpoints.length === 0 && userDetails.role === 1) {
|
||||||
$state.go('portainer.init.endpoint');
|
$state.go('portainer.init.endpoint');
|
||||||
} else if (endpoints.length === 0 && userDetails.role === 2) {
|
} else if (endpoints.length === 0 && userDetails.role === 2) {
|
||||||
|
|
|
@ -12,7 +12,10 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
|
||||||
URL: '',
|
URL: '',
|
||||||
PublicURL: '',
|
PublicURL: '',
|
||||||
GroupId: 1,
|
GroupId: 1,
|
||||||
SecurityFormData: new EndpointSecurityFormData()
|
SecurityFormData: new EndpointSecurityFormData(),
|
||||||
|
AzureApplicationId: '',
|
||||||
|
AzureTenantId: '',
|
||||||
|
AzureAuthenticationKey: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addDockerEndpoint = function() {
|
$scope.addDockerEndpoint = function() {
|
||||||
|
@ -30,7 +33,7 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
|
||||||
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
|
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
|
||||||
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
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() {
|
$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 publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
||||||
var groupId = $scope.formValues.GroupId;
|
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;
|
$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() {
|
.then(function success() {
|
||||||
Notifications.success('Endpoint created', name);
|
Notifications.success('Endpoint created', name);
|
||||||
$state.go('portainer.endpoints', {}, {reload: true});
|
$state.go('portainer.endpoints', {}, {reload: true});
|
||||||
|
|
|
@ -36,6 +36,16 @@
|
||||||
<p>Portainer agent</p>
|
<p>Portainer agent</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div ng-if="state.EnvironmentType === 'docker'">
|
<div ng-if="state.EnvironmentType === 'docker'">
|
||||||
|
@ -59,6 +69,28 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="col-sm-12 form-section-title">
|
||||||
Environment details
|
Environment details
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,35 +110,88 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
<!-- !name-input -->
|
||||||
<!-- endpoint-url-input -->
|
<!-- endpoint-url-input -->
|
||||||
<div class="form-group">
|
<div ng-if="state.EnvironmentType === 'docker' || state.EnvironmentType === 'agent'">
|
||||||
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
|
<div class="form-group">
|
||||||
Endpoint URL
|
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
<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>
|
Endpoint URL
|
||||||
</label>
|
<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>
|
||||||
<div class="col-sm-9 col-lg-10">
|
</label>
|
||||||
<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>
|
<div class="col-sm-9 col-lg-10">
|
||||||
<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>
|
<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>
|
<div class="form-group" ng-show="endpointCreationForm.endpoint_url.$invalid">
|
||||||
<div class="form-group" ng-show="endpointCreationForm.endpoint_url.$invalid">
|
<div class="col-sm-12 small text-danger">
|
||||||
<div class="col-sm-12 small text-danger">
|
<div ng-messages="endpointCreationForm.endpoint_url.$error">
|
||||||
<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>
|
||||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !endpoint-url-input -->
|
<!-- !endpoint-url-input -->
|
||||||
<!-- endpoint-public-url-input -->
|
<!-- endpoint-public-url-input -->
|
||||||
<div class="form-group">
|
<div ng-if="state.EnvironmentType === 'docker' || state.EnvironmentType === 'agent'">
|
||||||
<label for="endpoint_public_url" class="col-sm-3 col-lg-2 control-label text-left">
|
<div class="form-group">
|
||||||
Public IP
|
<label for="endpoint_public_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
<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>
|
Public IP
|
||||||
</label>
|
<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>
|
||||||
<div class="col-sm-9 col-lg-10">
|
</label>
|
||||||
<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 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>
|
||||||
</div>
|
</div>
|
||||||
<!-- !endpoint-public-url-input -->
|
<!-- !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 -->
|
<!-- group -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="endpoint_group" class="col-sm-3 col-lg-2 control-label text-left">
|
<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-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<!-- !actions -->
|
<!-- !actions -->
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- simple box -->
|
<!-- simple box -->
|
||||||
<div class="container simple-box">
|
<div class="container simple-box">
|
||||||
<div class="col-sm-10 col-sm-offset-1">
|
<div class="col-sm-12">
|
||||||
<!-- simple box logo -->
|
<!-- simple box logo -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||||
|
@ -55,6 +55,16 @@
|
||||||
<p>Connect to a Portainer agent</p>
|
<p>Connect to a Portainer agent</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<!-- !endpoint-type -->
|
<!-- !endpoint-type -->
|
||||||
|
@ -141,6 +151,78 @@
|
||||||
<!-- !actions -->
|
<!-- !actions -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !agent-endpoint -->
|
<!-- !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 -->
|
<!-- remote-endpoint -->
|
||||||
<div ng-if="formValues.EndpointType === 'remote'">
|
<div ng-if="formValues.EndpointType === 'remote'">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
|
@ -22,7 +22,10 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
||||||
TLSSKipClientVerify: false,
|
TLSSKipClientVerify: false,
|
||||||
TLSCACert: null,
|
TLSCACert: null,
|
||||||
TLSCert: null,
|
TLSCert: null,
|
||||||
TLSKey: null
|
TLSKey: null,
|
||||||
|
AzureApplicationId: '',
|
||||||
|
AzureTenantId: '',
|
||||||
|
AzureAuthenticationKey: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.createLocalEndpoint = function() {
|
$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() {
|
$scope.createAgentEndpoint = function() {
|
||||||
var name = $scope.formValues.Name;
|
var name = $scope.formValues.Name;
|
||||||
var URL = $scope.formValues.URL;
|
var URL = $scope.formValues.URL;
|
||||||
var PublicURL = URL.split(':')[0];
|
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() {
|
$scope.createRemoteEndpoint = function() {
|
||||||
|
@ -71,13 +83,34 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
||||||
var TLSCertFile = TLSSKipClientVerify ? null : $scope.formValues.TLSCert;
|
var TLSCertFile = TLSSKipClientVerify ? null : $scope.formValues.TLSCert;
|
||||||
var TLSKeyFile = TLSSKipClientVerify ? null : $scope.formValues.TLSKey;
|
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) {
|
function createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||||
var endpoint;
|
var endpoint;
|
||||||
$scope.state.actionInProgress = true;
|
$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) {
|
.then(function success(data) {
|
||||||
endpoint = data;
|
endpoint = data;
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
EndpointProvider.setEndpointID(endpoint.Id);
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
select-endpoint="switchEndpoint"
|
select-endpoint="switchEndpoint"
|
||||||
></endpoint-selector>
|
></endpoint-selector>
|
||||||
<li class="sidebar-title"><span class="endpoint-name">{{ activeEndpoint.Name }}</span></li>
|
<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"
|
endpoint-api-version="applicationState.endpoint.apiVersion"
|
||||||
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
|
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'"
|
standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"
|
||||||
|
|
|
@ -5,6 +5,29 @@ function ($q, $scope, $state, EndpointService, GroupService, StateManager, Endpo
|
||||||
$scope.switchEndpoint = function(endpoint) {
|
$scope.switchEndpoint = function(endpoint) {
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
EndpointProvider.setEndpointID(endpoint.Id);
|
||||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
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)
|
ExtensionManager.initEndpointExtensions(endpoint.Id)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var extensions = data;
|
var extensions = data;
|
||||||
|
@ -21,7 +44,7 @@ function ($q, $scope, $state, EndpointService, GroupService, StateManager, Endpo
|
||||||
EndpointProvider.setEndpointPublicURL(currentEndpoint.PublicURL);
|
EndpointProvider.setEndpointPublicURL(currentEndpoint.PublicURL);
|
||||||
return StateManager.updateEndpointState(true, currentEndpoint.Type, currentEndpoint.Extensions);
|
return StateManager.updateEndpointState(true, currentEndpoint.Type, currentEndpoint.Extensions);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
function setActiveEndpoint(endpoints) {
|
function setActiveEndpoint(endpoints) {
|
||||||
var activeEndpointID = EndpointProvider.endpointID();
|
var activeEndpointID = EndpointProvider.endpointID();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue