mirror of https://github.com/portainer/portainer
commit
8a9619c7e8
|
@ -0,0 +1,36 @@
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TarFileInBuffer will create a tar archive containing a single file named via fileName and using the content
|
||||||
|
// specified in fileContent. Returns the archive as a byte array.
|
||||||
|
func TarFileInBuffer(fileContent []byte, fileName string) ([]byte, error) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
tarWriter := tar.NewWriter(&buffer)
|
||||||
|
|
||||||
|
header := &tar.Header{
|
||||||
|
Name: fileName,
|
||||||
|
Mode: 0600,
|
||||||
|
Size: int64(len(fileContent)),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tarWriter.WriteHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tarWriter.Write(fileContent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tarWriter.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package bolt
|
||||||
|
|
||||||
|
import "github.com/portainer/portainer"
|
||||||
|
|
||||||
|
func (m *Migrator) updateEndpointsToVersion8() error {
|
||||||
|
legacyEndpoints, err := m.EndpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range legacyEndpoints {
|
||||||
|
endpoint.Extensions = []portainer.EndpointExtension{}
|
||||||
|
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -89,6 +89,13 @@ func (m *Migrator) Migrate() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.CurrentDBVersion < 8 {
|
||||||
|
err := m.updateEndpointsToVersion8()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
|
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -218,6 +218,7 @@ func main() {
|
||||||
},
|
},
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
|
Extensions: []portainer.EndpointExtension{},
|
||||||
}
|
}
|
||||||
err = store.EndpointService.CreateEndpoint(endpoint)
|
err = store.EndpointService.CreateEndpoint(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -57,6 +57,12 @@ const (
|
||||||
ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository")
|
ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Endpoint extensions error
|
||||||
|
const (
|
||||||
|
ErrEndpointExtensionNotSupported = Error("This extension is not supported")
|
||||||
|
ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint")
|
||||||
|
)
|
||||||
|
|
||||||
// Version errors.
|
// Version errors.
|
||||||
const (
|
const (
|
||||||
ErrDBVersionNotFound = Error("DB version not found")
|
ErrDBVersionNotFound = Error("DB version not found")
|
||||||
|
|
|
@ -28,13 +28,13 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []
|
||||||
for _, registry := range registries {
|
for _, registry := range registries {
|
||||||
if registry.Authentication {
|
if registry.Authentication {
|
||||||
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
||||||
runCommandAndCaptureStdErr(command, registryArgs, nil)
|
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dockerhub.Authentication {
|
if dockerhub.Authentication {
|
||||||
dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password)
|
dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password)
|
||||||
runCommandAndCaptureStdErr(command, dockerhubArgs, nil)
|
runCommandAndCaptureStdErr(command, dockerhubArgs, nil, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []
|
||||||
func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
|
func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
||||||
args = append(args, "logout")
|
args = append(args, "logout")
|
||||||
return runCommandAndCaptureStdErr(command, args, nil)
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deploy executes the docker stack deploy command.
|
// Deploy executes the docker stack deploy command.
|
||||||
|
@ -61,20 +61,21 @@ func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint
|
||||||
env = append(env, envvar.Name+"="+envvar.Value)
|
env = append(env, envvar.Name+"="+envvar.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runCommandAndCaptureStdErr(command, args, env)
|
return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove executes the docker stack rm command.
|
// Remove executes the docker stack rm command.
|
||||||
func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
||||||
args = append(args, "stack", "rm", stack.Name)
|
args = append(args, "stack", "rm", stack.Name)
|
||||||
return runCommandAndCaptureStdErr(command, args, nil)
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCommandAndCaptureStdErr(command string, args []string, env []string) error {
|
func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
cmd := exec.Command(command, args...)
|
cmd := exec.Command(command, args...)
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
cmd.Dir = workingDir
|
||||||
|
|
||||||
if env != nil {
|
if env != nil {
|
||||||
cmd.Env = os.Environ()
|
cmd.Env = os.Environ()
|
||||||
|
|
|
@ -35,24 +35,6 @@ func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *DockerHandler) checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool {
|
|
||||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
|
||||||
if authorizedUserID == userID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
memberships, _ := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
|
|
||||||
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
|
|
||||||
for _, membership := range memberships {
|
|
||||||
if membership.TeamID == authorizedTeamID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
id := vars["id"]
|
id := vars["id"]
|
||||||
|
@ -75,7 +57,14 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tokenData.Role != portainer.AdministratorRole && !handler.checkEndpointAccessControl(endpoint, tokenData.ID) {
|
|
||||||
|
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
|
||||||
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -85,7 +74,7 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,6 +136,7 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
||||||
},
|
},
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
|
Extensions: []portainer.EndpointExtension{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||||
|
@ -372,6 +373,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.ProxyManager.DeleteProxy(string(endpointID))
|
handler.ProxyManager.DeleteProxy(string(endpointID))
|
||||||
|
handler.ProxyManager.DeleteExtensionProxies(string(endpointID))
|
||||||
|
|
||||||
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
|
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtensionHandler represents an HTTP API handler for managing Settings.
|
||||||
|
type ExtensionHandler struct {
|
||||||
|
*mux.Router
|
||||||
|
Logger *log.Logger
|
||||||
|
EndpointService portainer.EndpointService
|
||||||
|
ProxyManager *proxy.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExtensionHandler returns a new instance of ExtensionHandler.
|
||||||
|
func NewExtensionHandler(bouncer *security.RequestBouncer) *ExtensionHandler {
|
||||||
|
h := &ExtensionHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
}
|
||||||
|
h.Handle("/{endpointId}/extensions",
|
||||||
|
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostExtensions))).Methods(http.MethodPost)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
postExtensionRequest struct {
|
||||||
|
Type int `valid:"required"`
|
||||||
|
URL string `valid:"required"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (handler *ExtensionHandler) handlePostExtensions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id, err := strconv.Atoi(vars["endpointId"])
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
endpointID := portainer.EndpointID(id)
|
||||||
|
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||||
|
if err == portainer.ErrEndpointNotFound {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req postExtensionRequest
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
extensionType := portainer.EndpointExtensionType(req.Type)
|
||||||
|
|
||||||
|
for _, extension := range endpoint.Extensions {
|
||||||
|
if extension.Type == extensionType {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrEndpointExtensionAlreadyAssociated, http.StatusConflict, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension := portainer.EndpointExtension{
|
||||||
|
Type: extensionType,
|
||||||
|
URL: req.URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.Extensions = append(endpoint.Extensions, extension)
|
||||||
|
|
||||||
|
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeJSON(w, extension, handler.Logger)
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package extensions
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StoridgeHandler represents an HTTP API handler for proxying requests to the Docker API.
|
||||||
|
type StoridgeHandler struct {
|
||||||
|
*mux.Router
|
||||||
|
Logger *log.Logger
|
||||||
|
EndpointService portainer.EndpointService
|
||||||
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
|
ProxyManager *proxy.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStoridgeHandler returns a new instance of StoridgeHandler.
|
||||||
|
func NewStoridgeHandler(bouncer *security.RequestBouncer) *StoridgeHandler {
|
||||||
|
h := &StoridgeHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
}
|
||||||
|
h.PathPrefix("/{id}/extensions/storidge").Handler(
|
||||||
|
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToStoridgeAPI)))
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *StoridgeHandler) proxyRequestsToStoridgeAPI(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
|
||||||
|
}
|
||||||
|
|
||||||
|
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var storidgeExtension *portainer.EndpointExtension
|
||||||
|
for _, extension := range endpoint.Extensions {
|
||||||
|
if extension.Type == portainer.StoridgeEndpointExtension {
|
||||||
|
storidgeExtension = &extension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if storidgeExtension == nil {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrEndpointExtensionNotSupported, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension)
|
||||||
|
|
||||||
|
var proxy http.Handler
|
||||||
|
proxy = handler.ProxyManager.GetExtensionProxy(proxyExtensionKey)
|
||||||
|
if proxy == nil {
|
||||||
|
proxy, err = handler.ProxyManager.CreateAndRegisterExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.StripPrefix("/"+id+"/extensions/storidge", proxy).ServeHTTP(w, r)
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
httperror "github.com/portainer/portainer/http/error"
|
httperror "github.com/portainer/portainer/http/error"
|
||||||
|
"github.com/portainer/portainer/http/handler/extensions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler is a collection of all the service handlers.
|
// Handler is a collection of all the service handlers.
|
||||||
|
@ -19,6 +20,8 @@ type Handler struct {
|
||||||
EndpointHandler *EndpointHandler
|
EndpointHandler *EndpointHandler
|
||||||
RegistryHandler *RegistryHandler
|
RegistryHandler *RegistryHandler
|
||||||
DockerHubHandler *DockerHubHandler
|
DockerHubHandler *DockerHubHandler
|
||||||
|
ExtensionHandler *ExtensionHandler
|
||||||
|
StoridgeHandler *extensions.StoridgeHandler
|
||||||
ResourceHandler *ResourceHandler
|
ResourceHandler *ResourceHandler
|
||||||
StackHandler *StackHandler
|
StackHandler *StackHandler
|
||||||
StatusHandler *StatusHandler
|
StatusHandler *StatusHandler
|
||||||
|
@ -48,11 +51,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
|
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
|
||||||
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
|
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
|
||||||
if strings.Contains(r.URL.Path, "/docker/") {
|
switch {
|
||||||
|
case strings.Contains(r.URL.Path, "/docker"):
|
||||||
http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r)
|
||||||
} else if strings.Contains(r.URL.Path, "/stacks") {
|
case strings.Contains(r.URL.Path, "/stacks"):
|
||||||
http.StripPrefix("/api/endpoints", h.StackHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api/endpoints", h.StackHandler).ServeHTTP(w, r)
|
||||||
} else {
|
case strings.Contains(r.URL.Path, "/extensions/storidge"):
|
||||||
|
http.StripPrefix("/api/endpoints", h.StoridgeHandler).ServeHTTP(w, r)
|
||||||
|
case strings.Contains(r.URL.Path, "/extensions"):
|
||||||
|
http.StripPrefix("/api/endpoints", h.ExtensionHandler).ServeHTTP(w, r)
|
||||||
|
default:
|
||||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/archive"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postDockerfileRequest struct {
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOperation inspects the "Content-Type" header to determine if it needs to alter the request.
|
||||||
|
// If the value of the header is empty, it means that a Dockerfile is posted via upload, the function
|
||||||
|
// will extract the file content from the request body, tar it, and rewrite the body.
|
||||||
|
// If the value of the header contains "application/json", it means that the content of a Dockerfile is posted
|
||||||
|
// in the request payload as JSON, the function will create a new file called Dockerfile inside a tar archive and
|
||||||
|
// rewrite the body of the request.
|
||||||
|
// In any other case, it will leave the request unaltered.
|
||||||
|
func buildOperation(request *http.Request) error {
|
||||||
|
contentTypeHeader := request.Header.Get("Content-Type")
|
||||||
|
if contentTypeHeader != "" && !strings.Contains(contentTypeHeader, "application/json") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var dockerfileContent []byte
|
||||||
|
|
||||||
|
if contentTypeHeader == "" {
|
||||||
|
body, err := ioutil.ReadAll(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dockerfileContent = body
|
||||||
|
} else {
|
||||||
|
var req postDockerfileRequest
|
||||||
|
if err := json.NewDecoder(request.Body).Decode(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dockerfileContent = []byte(req.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer, err := archive.TarFileInBuffer(dockerfileContent, "Dockerfile")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Body = ioutil.NopCloser(bytes.NewReader(buffer))
|
||||||
|
request.ContentLength = int64(len(buffer))
|
||||||
|
request.Header.Set("Content-Type", "application/x-tar")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -17,14 +17,14 @@ type proxyFactory struct {
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
func (factory *proxyFactory) newExtensionHTTPPRoxy(u *url.URL) http.Handler {
|
||||||
u.Scheme = "http"
|
u.Scheme = "http"
|
||||||
return factory.createReverseProxy(u)
|
return newSingleHostReverseProxyWithHostHeader(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
u.Scheme = "https"
|
u.Scheme = "https"
|
||||||
proxy := factory.createReverseProxy(u)
|
proxy := factory.createDockerReverseProxy(u)
|
||||||
config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -34,7 +34,12 @@ func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpo
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
|
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL) http.Handler {
|
||||||
|
u.Scheme = "http"
|
||||||
|
return factory.createDockerReverseProxy(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (factory *proxyFactory) newDockerSocketProxy(path string) http.Handler {
|
||||||
proxy := &socketProxy{}
|
proxy := &socketProxy{}
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
ResourceControlService: factory.ResourceControlService,
|
ResourceControlService: factory.ResourceControlService,
|
||||||
|
@ -46,13 +51,13 @@ func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) createReverseProxy(u *url.URL) *httputil.ReverseProxy {
|
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL) *httputil.ReverseProxy {
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
ResourceControlService: factory.ResourceControlService,
|
ResourceControlService: factory.ResourceControlService,
|
||||||
TeamMembershipService: factory.TeamMembershipService,
|
TeamMembershipService: factory.TeamMembershipService,
|
||||||
SettingsService: factory.SettingsService,
|
SettingsService: factory.SettingsService,
|
||||||
dockerTransport: newHTTPTransport(),
|
dockerTransport: &http.Transport{},
|
||||||
}
|
}
|
||||||
proxy.Transport = transport
|
proxy.Transport = transport
|
||||||
return proxy
|
return proxy
|
||||||
|
@ -65,7 +70,3 @@ func newSocketTransport(socketPath string) *http.Transport {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPTransport() *http.Transport {
|
|
||||||
return &http.Transport{}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package proxy
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/orcaman/concurrent-map"
|
"github.com/orcaman/concurrent-map"
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
@ -12,12 +13,14 @@ import (
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
proxyFactory *proxyFactory
|
proxyFactory *proxyFactory
|
||||||
proxies cmap.ConcurrentMap
|
proxies cmap.ConcurrentMap
|
||||||
|
extensionProxies cmap.ConcurrentMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager initializes a new proxy Service
|
// NewManager initializes a new proxy Service
|
||||||
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService) *Manager {
|
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
proxies: cmap.New(),
|
proxies: cmap.New(),
|
||||||
|
extensionProxies: cmap.New(),
|
||||||
proxyFactory: &proxyFactory{
|
proxyFactory: &proxyFactory{
|
||||||
ResourceControlService: resourceControlService,
|
ResourceControlService: resourceControlService,
|
||||||
TeamMembershipService: teamMembershipService,
|
TeamMembershipService: teamMembershipService,
|
||||||
|
@ -38,16 +41,16 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
|
||||||
|
|
||||||
if endpointURL.Scheme == "tcp" {
|
if endpointURL.Scheme == "tcp" {
|
||||||
if endpoint.TLSConfig.TLS {
|
if endpoint.TLSConfig.TLS {
|
||||||
proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
|
proxy, err = manager.proxyFactory.newDockerHTTPSProxy(endpointURL, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
proxy = manager.proxyFactory.newHTTPProxy(endpointURL)
|
proxy = manager.proxyFactory.newDockerHTTPProxy(endpointURL)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Assume unix:// scheme
|
// Assume unix:// scheme
|
||||||
proxy = manager.proxyFactory.newSocketProxy(endpointURL.Path)
|
proxy = manager.proxyFactory.newDockerSocketProxy(endpointURL.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.proxies.Set(string(endpoint.ID), proxy)
|
manager.proxies.Set(string(endpoint.ID), proxy)
|
||||||
|
@ -67,3 +70,34 @@ func (manager *Manager) GetProxy(key string) http.Handler {
|
||||||
func (manager *Manager) DeleteProxy(key string) {
|
func (manager *Manager) DeleteProxy(key string) {
|
||||||
manager.proxies.Remove(key)
|
manager.proxies.Remove(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAndRegisterExtensionProxy creates a new HTTP reverse proxy for an extension and adds it to the registered proxies.
|
||||||
|
func (manager *Manager) CreateAndRegisterExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
||||||
|
|
||||||
|
extensionURL, err := url.Parse(extensionAPIURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := manager.proxyFactory.newExtensionHTTPPRoxy(extensionURL)
|
||||||
|
manager.extensionProxies.Set(key, proxy)
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExtensionProxy returns the extension proxy associated to a key
|
||||||
|
func (manager *Manager) GetExtensionProxy(key string) http.Handler {
|
||||||
|
proxy, ok := manager.extensionProxies.Get(key)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return proxy.(http.Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteExtensionProxies deletes all the extension proxies associated to a key
|
||||||
|
func (manager *Manager) DeleteExtensionProxies(key string) {
|
||||||
|
for _, k := range manager.extensionProxies.Keys() {
|
||||||
|
if strings.Contains(k, key+"_") {
|
||||||
|
manager.extensionProxies.Remove(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ type (
|
||||||
labelBlackList []portainer.Pair
|
labelBlackList []portainer.Pair
|
||||||
}
|
}
|
||||||
restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
|
restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
|
||||||
|
operationRequest func(*http.Request) error
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
@ -59,6 +60,8 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
|
||||||
return p.proxyNodeRequest(request)
|
return p.proxyNodeRequest(request)
|
||||||
case strings.HasPrefix(path, "/tasks"):
|
case strings.HasPrefix(path, "/tasks"):
|
||||||
return p.proxyTaskRequest(request)
|
return p.proxyTaskRequest(request)
|
||||||
|
case strings.HasPrefix(path, "/build"):
|
||||||
|
return p.proxyBuildRequest(request)
|
||||||
default:
|
default:
|
||||||
return p.executeDockerRequest(request)
|
return p.executeDockerRequest(request)
|
||||||
}
|
}
|
||||||
|
@ -228,6 +231,10 @@ func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *proxyTransport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
return p.interceptAndRewriteRequest(request, buildOperation)
|
||||||
|
}
|
||||||
|
|
||||||
// restrictedOperation ensures that the current user has the required authorizations
|
// restrictedOperation ensures that the current user has the required authorizations
|
||||||
// before executing the original request.
|
// before executing the original request.
|
||||||
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
||||||
|
@ -300,6 +307,15 @@ func (p *proxyTransport) rewriteOperation(request *http.Request, operation restr
|
||||||
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *proxyTransport) interceptAndRewriteRequest(request *http.Request, operation operationRequest) (*http.Response, error) {
|
||||||
|
err := operation(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
|
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
|
||||||
response, err := p.executeDockerRequest(request)
|
response, err := p.executeDockerRequest(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -121,3 +121,22 @@ func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedReques
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthorizedEndpointAccess ensure that the user can access the specified endpoint.
|
||||||
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
|
// listed in the authorized teams.
|
||||||
|
func AuthorizedEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||||
|
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||||
|
if authorizedUserID == userID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, membership := range memberships {
|
||||||
|
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
|
||||||
|
if membership.TeamID == authorizedTeamID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ type (
|
||||||
// RequestBouncer represents an entity that manages API request accesses
|
// RequestBouncer represents an entity that manages API request accesses
|
||||||
RequestBouncer struct {
|
RequestBouncer struct {
|
||||||
jwtService portainer.JWTService
|
jwtService portainer.JWTService
|
||||||
|
userService portainer.UserService
|
||||||
teamMembershipService portainer.TeamMembershipService
|
teamMembershipService portainer.TeamMembershipService
|
||||||
authDisabled bool
|
authDisabled bool
|
||||||
}
|
}
|
||||||
|
@ -27,9 +28,10 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRequestBouncer initializes a new RequestBouncer
|
// NewRequestBouncer initializes a new RequestBouncer
|
||||||
func NewRequestBouncer(jwtService portainer.JWTService, teamMembershipService portainer.TeamMembershipService, authDisabled bool) *RequestBouncer {
|
func NewRequestBouncer(jwtService portainer.JWTService, userService portainer.UserService, teamMembershipService portainer.TeamMembershipService, authDisabled bool) *RequestBouncer {
|
||||||
return &RequestBouncer{
|
return &RequestBouncer{
|
||||||
jwtService: jwtService,
|
jwtService: jwtService,
|
||||||
|
userService: userService,
|
||||||
teamMembershipService: teamMembershipService,
|
teamMembershipService: teamMembershipService,
|
||||||
authDisabled: authDisabled,
|
authDisabled: authDisabled,
|
||||||
}
|
}
|
||||||
|
@ -136,6 +138,15 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusUnauthorized, nil)
|
httperror.WriteErrorResponse(w, err, http.StatusUnauthorized, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = bouncer.userService.User(tokenData.ID)
|
||||||
|
if err != nil && err == portainer.ErrUserNotFound {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tokenData = &portainer.TokenData{
|
tokenData = &portainer.TokenData{
|
||||||
Role: portainer.AdministratorRole,
|
Role: portainer.AdministratorRole,
|
||||||
|
|
|
@ -3,6 +3,7 @@ package http
|
||||||
import (
|
import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
"github.com/portainer/portainer/http/handler"
|
"github.com/portainer/portainer/http/handler"
|
||||||
|
"github.com/portainer/portainer/http/handler/extensions"
|
||||||
"github.com/portainer/portainer/http/proxy"
|
"github.com/portainer/portainer/http/proxy"
|
||||||
"github.com/portainer/portainer/http/security"
|
"github.com/portainer/portainer/http/security"
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ type Server struct {
|
||||||
|
|
||||||
// Start starts the HTTP server
|
// Start starts the HTTP server
|
||||||
func (server *Server) Start() error {
|
func (server *Server) Start() error {
|
||||||
requestBouncer := security.NewRequestBouncer(server.JWTService, server.TeamMembershipService, server.AuthDisabled)
|
requestBouncer := security.NewRequestBouncer(server.JWTService, server.UserService, server.TeamMembershipService, server.AuthDisabled)
|
||||||
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService)
|
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService)
|
||||||
|
|
||||||
var fileHandler = handler.NewFileHandler(filepath.Join(server.AssetsPath, "public"))
|
var fileHandler = handler.NewFileHandler(filepath.Join(server.AssetsPath, "public"))
|
||||||
|
@ -96,6 +97,13 @@ func (server *Server) Start() error {
|
||||||
stackHandler.GitService = server.GitService
|
stackHandler.GitService = server.GitService
|
||||||
stackHandler.RegistryService = server.RegistryService
|
stackHandler.RegistryService = server.RegistryService
|
||||||
stackHandler.DockerHubService = server.DockerHubService
|
stackHandler.DockerHubService = server.DockerHubService
|
||||||
|
var extensionHandler = handler.NewExtensionHandler(requestBouncer)
|
||||||
|
extensionHandler.EndpointService = server.EndpointService
|
||||||
|
extensionHandler.ProxyManager = proxyManager
|
||||||
|
var storidgeHandler = extensions.NewStoridgeHandler(requestBouncer)
|
||||||
|
storidgeHandler.EndpointService = server.EndpointService
|
||||||
|
storidgeHandler.TeamMembershipService = server.TeamMembershipService
|
||||||
|
storidgeHandler.ProxyManager = proxyManager
|
||||||
|
|
||||||
server.Handler = &handler.Handler{
|
server.Handler = &handler.Handler{
|
||||||
AuthHandler: authHandler,
|
AuthHandler: authHandler,
|
||||||
|
@ -114,6 +122,8 @@ func (server *Server) Start() error {
|
||||||
WebSocketHandler: websocketHandler,
|
WebSocketHandler: websocketHandler,
|
||||||
FileHandler: fileHandler,
|
FileHandler: fileHandler,
|
||||||
UploadHandler: uploadHandler,
|
UploadHandler: uploadHandler,
|
||||||
|
ExtensionHandler: extensionHandler,
|
||||||
|
StoridgeHandler: storidgeHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
if server.SSL {
|
if server.SSL {
|
||||||
|
|
|
@ -178,6 +178,7 @@ type (
|
||||||
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"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
|
@ -187,6 +188,16 @@ type (
|
||||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EndpointExtension represents a extension associated to an endpoint.
|
||||||
|
EndpointExtension struct {
|
||||||
|
Type EndpointExtensionType `json:"Type"`
|
||||||
|
URL string `json:"URL"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndpointExtensionType represents the type of an endpoint extension. Only
|
||||||
|
// one extension of each type can be associated to an endpoint.
|
||||||
|
EndpointExtensionType int
|
||||||
|
|
||||||
// ResourceControlID represents a resource control identifier.
|
// ResourceControlID represents a resource control identifier.
|
||||||
ResourceControlID int
|
ResourceControlID int
|
||||||
|
|
||||||
|
@ -389,9 +400,9 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API.
|
// APIVersion is the version number of the Portainer API.
|
||||||
APIVersion = "1.16.2"
|
APIVersion = "1.16.3"
|
||||||
// DBVersion is the version number of the Portainer database.
|
// DBVersion is the version number of the Portainer database.
|
||||||
DBVersion = 7
|
DBVersion = 8
|
||||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||||
)
|
)
|
||||||
|
@ -452,3 +463,9 @@ const (
|
||||||
// ConfigResourceControl represents a resource control associated to a Docker config
|
// ConfigResourceControl represents a resource control associated to a Docker config
|
||||||
ConfigResourceControl
|
ConfigResourceControl
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ EndpointExtensionType = iota
|
||||||
|
// StoridgeEndpointExtension represents the Storidge extension
|
||||||
|
StoridgeEndpointExtension
|
||||||
|
)
|
||||||
|
|
|
@ -56,7 +56,7 @@ info:
|
||||||
|
|
||||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||||
|
|
||||||
version: "1.16.2"
|
version: "1.16.3"
|
||||||
title: "Portainer API"
|
title: "Portainer API"
|
||||||
contact:
|
contact:
|
||||||
email: "info@portainer.io"
|
email: "info@portainer.io"
|
||||||
|
@ -2143,7 +2143,7 @@ definitions:
|
||||||
description: "Is analytics enabled"
|
description: "Is analytics enabled"
|
||||||
Version:
|
Version:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "1.16.2"
|
example: "1.16.3"
|
||||||
description: "Portainer API version"
|
description: "Portainer API version"
|
||||||
PublicSettingsInspectResponse:
|
PublicSettingsInspectResponse:
|
||||||
type: "object"
|
type: "object"
|
||||||
|
|
|
@ -13,6 +13,8 @@ angular.module('portainer', [
|
||||||
'angular-google-analytics',
|
'angular-google-analytics',
|
||||||
'angular-json-tree',
|
'angular-json-tree',
|
||||||
'angular-loading-bar',
|
'angular-loading-bar',
|
||||||
|
'angular-clipboard',
|
||||||
|
'luegg.directives',
|
||||||
'portainer.templates',
|
'portainer.templates',
|
||||||
'portainer.app',
|
'portainer.app',
|
||||||
'portainer.docker',
|
'portainer.docker',
|
||||||
|
|
|
@ -179,6 +179,17 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var imageBuild = {
|
||||||
|
name: 'docker.images.build',
|
||||||
|
url: '/build',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/docker/views/images/build/buildimage.html',
|
||||||
|
controller: 'BuildImageController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var networks = {
|
var networks = {
|
||||||
name: 'docker.networks',
|
name: 'docker.networks',
|
||||||
url: '/networks',
|
url: '/networks',
|
||||||
|
@ -378,6 +389,17 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var taskLogs = {
|
||||||
|
name: 'docker.tasks.task.logs',
|
||||||
|
url: '/logs',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/docker/views/tasks/logs/tasklogs.html',
|
||||||
|
controller: 'TaskLogsController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var templates = {
|
var templates = {
|
||||||
name: 'docker.templates',
|
name: 'docker.templates',
|
||||||
url: '/templates',
|
url: '/templates',
|
||||||
|
@ -457,6 +479,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||||
$stateRegistryProvider.register(events);
|
$stateRegistryProvider.register(events);
|
||||||
$stateRegistryProvider.register(images);
|
$stateRegistryProvider.register(images);
|
||||||
$stateRegistryProvider.register(image);
|
$stateRegistryProvider.register(image);
|
||||||
|
$stateRegistryProvider.register(imageBuild);
|
||||||
$stateRegistryProvider.register(networks);
|
$stateRegistryProvider.register(networks);
|
||||||
$stateRegistryProvider.register(network);
|
$stateRegistryProvider.register(network);
|
||||||
$stateRegistryProvider.register(networkCreation);
|
$stateRegistryProvider.register(networkCreation);
|
||||||
|
@ -476,6 +499,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||||
$stateRegistryProvider.register(swarmVisualizer);
|
$stateRegistryProvider.register(swarmVisualizer);
|
||||||
$stateRegistryProvider.register(tasks);
|
$stateRegistryProvider.register(tasks);
|
||||||
$stateRegistryProvider.register(task);
|
$stateRegistryProvider.register(task);
|
||||||
|
$stateRegistryProvider.register(taskLogs);
|
||||||
$stateRegistryProvider.register(templates);
|
$stateRegistryProvider.register(templates);
|
||||||
$stateRegistryProvider.register(templatesLinuxServer);
|
$stateRegistryProvider.register(templatesLinuxServer);
|
||||||
$stateRegistryProvider.register(volumes);
|
$stateRegistryProvider.register(volumes);
|
||||||
|
|
|
@ -195,10 +195,10 @@
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect">
|
<td ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect">
|
||||||
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
|
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
|
||||||
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="docker.containers.container.stats({id: item.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="docker.containers.container.stats({id: item.Id})" title="Stats"><i class="fa fa-area-chart space-right" aria-hidden="true"></i></a>
|
||||||
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({id: item.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({id: item.Id})" title="Logs"><i class="fa fa-file-text-o space-right" aria-hidden="true"></i></a>
|
||||||
<a ng-if="$ctrl.settings.showQuickActionConsole" style="margin: 0 2.5px;" ui-sref="docker.containers.container.console({id: item.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionConsole" style="margin: 0 2.5px;" ui-sref="docker.containers.container.console({id: item.Id})" title="Console"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
|
||||||
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="docker.containers.container.inspect({id: item.Id})"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="docker.containers.container.inspect({id: item.Id})" title="Inspect"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||||
|
|
|
@ -25,6 +25,9 @@
|
||||||
<li><a ng-click="$ctrl.forceRemoveAction($ctrl.state.selectedItems, true)" ng-disabled="$ctrl.state.selectedItemCount === 0">Force Remove</a></li>
|
<li><a ng-click="$ctrl.forceRemoveAction($ctrl.state.selectedItems, true)" ng-disabled="$ctrl.state.selectedItemCount === 0">Force Remove</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.images.build">
|
||||||
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Build a new image
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
|
|
@ -114,7 +114,7 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a ng-if="item.Ports && item.Ports.length > 0 && $ctrl.swarmManagerIp && p.PublishedPort" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.swarmManagerIp }}:{{ p.PublishedPort }}" target="_blank">
|
<a ng-if="item.Ports && item.Ports.length > 0 && p.PublishedPort" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.publicUrl }}:{{ p.PublishedPort }}" target="_blank">
|
||||||
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }}
|
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }}
|
||||||
</a>
|
</a>
|
||||||
<span ng-if="!item.Ports || item.Ports.length === 0 || !$ctrl.swarmManagerIp" >-</span>
|
<span ng-if="!item.Ports || item.Ports.length === 0 || !$ctrl.swarmManagerIp" >-</span>
|
||||||
|
|
|
@ -12,7 +12,7 @@ angular.module('portainer.docker').component('servicesDatatable', {
|
||||||
showOwnershipColumn: '<',
|
showOwnershipColumn: '<',
|
||||||
removeAction: '<',
|
removeAction: '<',
|
||||||
scaleAction: '<',
|
scaleAction: '<',
|
||||||
swarmManagerIp: '<',
|
publicUrl: '<',
|
||||||
forceUpdateAction: '<',
|
forceUpdateAction: '<',
|
||||||
showForceUpdateButton: '<'
|
showForceUpdateButton: '<'
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"></i>
|
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"></i>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th ng-if="$ctrl.showLogsButton">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -63,6 +64,11 @@
|
||||||
<td ng-if="$ctrl.showSlotColumn">{{ item.Slot ? item.Slot : '-' }}</td>
|
<td ng-if="$ctrl.showSlotColumn">{{ item.Slot ? item.Slot : '-' }}</td>
|
||||||
<td>{{ item.NodeId | tasknodename: $ctrl.nodes }}</td>
|
<td>{{ item.NodeId | tasknodename: $ctrl.nodes }}</td>
|
||||||
<td>{{ item.Updated | getisodate }}</td>
|
<td>{{ item.Updated | getisodate }}</td>
|
||||||
|
<td ng-if="$ctrl.showLogsButton">
|
||||||
|
<a ui-sref="docker.tasks.task.logs({id: item.Id})">
|
||||||
|
<i class="fa fa-file-text-o" aria-hidden="true"></i> View logs
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!$ctrl.dataset">
|
<tr ng-if="!$ctrl.dataset">
|
||||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||||
|
|
|
@ -10,6 +10,7 @@ angular.module('portainer.docker').component('tasksDatatable', {
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
nodes: '<',
|
nodes: '<',
|
||||||
showTextFilter: '<',
|
showTextFilter: '<',
|
||||||
showSlotColumn: '<'
|
showSlotColumn: '<',
|
||||||
|
showLogsButton: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
angular.module('portainer.docker').component('logViewer', {
|
||||||
|
templateUrl: 'app/docker/components/log-viewer/logViewer.html',
|
||||||
|
controller: 'LogViewerController',
|
||||||
|
bindings: {
|
||||||
|
data: '=',
|
||||||
|
logCollectionChange: '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,62 @@
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-file-text-o" title="Log viewer settings"></rd-widget-header>
|
||||||
|
<rd-widget-body>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="tls" class="control-label text-left">
|
||||||
|
Log collection
|
||||||
|
<portainer-tooltip position="bottom" message="Disabling this option allows you to pause the log collection process."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" ng-model="$ctrl.state.logCollection" ng-change="$ctrl.logCollectionChange($ctrl.state.logCollection)"><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="tls" class="control-label text-left">
|
||||||
|
Auto-scrolling
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" ng-model="$ctrl.state.autoScroll"><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="logs_search" class="col-sm-1 control-label text-left">
|
||||||
|
Search
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<input class="form-control" type="text" name="logs_search" ng-model="$ctrl.state.search" ng-change="$ctrl.state.selectedLines.length = 0;" placeholder="Filter...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.copySupported">
|
||||||
|
<label class="col-sm-1 control-label text-left">
|
||||||
|
Actions
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<button class="btn btn-primary btn-sm" ng-click="$ctrl.copy()" ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]) || !$ctrl.state.filteredLogs.length"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</button>
|
||||||
|
<button class="btn btn-primary btn-sm" ng-click="$ctrl.copySelection()" ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]) || !$ctrl.state.filteredLogs.length || !$ctrl.state.selectedLines.length"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy selected lines</button>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" style="height:54%;">
|
||||||
|
<div class="col-sm-12" style="height:100%;">
|
||||||
|
<pre class="log_viewer" scroll-glue="$ctrl.state.autoScroll">
|
||||||
|
<div ng-repeat="line in $ctrl.state.filteredLogs = ($ctrl.data | filter:$ctrl.state.search) track by $index" class="line" ng-if="line"><p class="inner_line" ng-click="active=!active; $ctrl.selectLine(line)" ng-class="{'line_selected': active}">{{ line }}</p></div>
|
||||||
|
<div ng-if="!$ctrl.state.filteredLogs.length" class="line"><p class="inner_line">No log line matching the '{{ $ctrl.state.search }}' filter</p></div>
|
||||||
|
<div ng-if="$ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]" class="line"><p class="inner_line">No logs available</p></div>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,35 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.controller('LogViewerController', ['clipboard',
|
||||||
|
function (clipboard) {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
copySupported: clipboard.supported,
|
||||||
|
logCollection: true,
|
||||||
|
autoScroll: true,
|
||||||
|
search: '',
|
||||||
|
filteredLogs: [],
|
||||||
|
selectedLines: []
|
||||||
|
};
|
||||||
|
|
||||||
|
this.copy = function() {
|
||||||
|
clipboard.copyText(this.state.filteredLogs);
|
||||||
|
$('#refreshRateChange').show();
|
||||||
|
$('#refreshRateChange').fadeOut(1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.copySelection = function() {
|
||||||
|
clipboard.copyText(this.state.selectedLines);
|
||||||
|
$('#refreshRateChange').show();
|
||||||
|
$('#refreshRateChange').fadeOut(1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.selectLine = function(line) {
|
||||||
|
var idx = this.state.selectedLines.indexOf(line);
|
||||||
|
if (idx === -1) {
|
||||||
|
this.state.selectedLines.push(line);
|
||||||
|
} else {
|
||||||
|
this.state.selectedLines.splice(idx, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -4,6 +4,22 @@ function includeString(text, values) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function strToHash(str) {
|
||||||
|
var hash = 0;
|
||||||
|
for (var i = 0; i < str.length; i++) {
|
||||||
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashToHexColor(hash) {
|
||||||
|
var color = '#';
|
||||||
|
for (var i = 0; i < 3;) {
|
||||||
|
color += ('00' + ((hash >> i++ * 8) & 0xFF).toString(16)).slice(-2);
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.filter('visualizerTask', function () {
|
.filter('visualizerTask', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
@ -19,6 +35,14 @@ angular.module('portainer.docker')
|
||||||
return 'running';
|
return 'running';
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
.filter('visualizerTaskBorderColor', function () {
|
||||||
|
'use strict';
|
||||||
|
return function (str) {
|
||||||
|
var hash = strToHash(str);
|
||||||
|
var color = hashToHexColor(hash);
|
||||||
|
return color;
|
||||||
|
};
|
||||||
|
})
|
||||||
.filter('taskstatusbadge', function () {
|
.filter('taskstatusbadge', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (text) {
|
return function (text) {
|
||||||
|
@ -42,11 +66,11 @@ angular.module('portainer.docker')
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (text) {
|
return function (text) {
|
||||||
var status = _.toLower(text);
|
var status = _.toLower(text);
|
||||||
if (includeString(status, ['paused', 'starting'])) {
|
if (includeString(status, ['paused', 'starting', 'unhealthy'])) {
|
||||||
return 'warning';
|
return 'warning';
|
||||||
} else if (includeString(status, ['created'])) {
|
} else if (includeString(status, ['created'])) {
|
||||||
return 'info';
|
return 'info';
|
||||||
} else if (includeString(status, ['stopped', 'unhealthy', 'dead', 'exited'])) {
|
} else if (includeString(status, ['stopped', 'dead', 'exited'])) {
|
||||||
return 'danger';
|
return 'danger';
|
||||||
}
|
}
|
||||||
return 'success';
|
return 'success';
|
||||||
|
|
|
@ -9,16 +9,6 @@ angular.module('portainer.docker')
|
||||||
Labels: node.Spec.Labels,
|
Labels: node.Spec.Labels,
|
||||||
Availability: node.Spec.Availability
|
Availability: node.Spec.Availability
|
||||||
};
|
};
|
||||||
},
|
|
||||||
getManagerIP: function(nodes) {
|
|
||||||
var managerIp;
|
|
||||||
for (var n in nodes) {
|
|
||||||
if (undefined === nodes[n].ManagerStatus || nodes[n].ManagerStatus.Reachability !== 'reachable') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
managerIp = nodes[n].ManagerStatus.Addr.split(':')[0];
|
|
||||||
}
|
|
||||||
return managerIp;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -36,3 +36,35 @@ function ContainerViewModel(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ContainerStatsViewModel(data) {
|
||||||
|
this.Date = data.read;
|
||||||
|
this.MemoryUsage = data.memory_stats.usage;
|
||||||
|
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
|
||||||
|
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
|
||||||
|
this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
|
||||||
|
this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
|
||||||
|
if (data.cpu_stats.cpu_usage.percpu_usage) {
|
||||||
|
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
|
||||||
|
}
|
||||||
|
this.Networks = _.values(data.networks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContainerDetailsViewModel(data) {
|
||||||
|
this.Model = data;
|
||||||
|
this.Id = data.Id;
|
||||||
|
this.State = data.State;
|
||||||
|
this.Created = data.Created;
|
||||||
|
this.Name = data.Name;
|
||||||
|
this.NetworkSettings = data.NetworkSettings;
|
||||||
|
this.Args = data.Args;
|
||||||
|
this.Image = data.Image;
|
||||||
|
this.Config = data.Config;
|
||||||
|
this.HostConfig = data.HostConfig;
|
||||||
|
this.Mounts = data.Mounts;
|
||||||
|
if (data.Portainer) {
|
||||||
|
if (data.Portainer.ResourceControl) {
|
||||||
|
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
function ContainerDetailsViewModel(data) {
|
|
||||||
this.Model = data;
|
|
||||||
this.Id = data.Id;
|
|
||||||
this.State = data.State;
|
|
||||||
this.Created = data.Created;
|
|
||||||
this.Name = data.Name;
|
|
||||||
this.NetworkSettings = data.NetworkSettings;
|
|
||||||
this.Args = data.Args;
|
|
||||||
this.Image = data.Image;
|
|
||||||
this.Config = data.Config;
|
|
||||||
this.HostConfig = data.HostConfig;
|
|
||||||
this.Mounts = data.Mounts;
|
|
||||||
if (data.Portainer) {
|
|
||||||
if (data.Portainer.ResourceControl) {
|
|
||||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
function ContainerStatsViewModel(data) {
|
|
||||||
this.Date = data.read;
|
|
||||||
this.MemoryUsage = data.memory_stats.usage;
|
|
||||||
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
|
|
||||||
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
|
|
||||||
this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
|
|
||||||
this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
|
|
||||||
if (data.cpu_stats.cpu_usage.percpu_usage) {
|
|
||||||
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
|
|
||||||
}
|
|
||||||
this.Networks = _.values(data.networks);
|
|
||||||
}
|
|
|
@ -37,6 +37,33 @@ function createEventDetails(event) {
|
||||||
case 'attach':
|
case 'attach':
|
||||||
details = 'Container ' + eventAttr.name + ' attached';
|
details = 'Container ' + eventAttr.name + ' attached';
|
||||||
break;
|
break;
|
||||||
|
case 'detach':
|
||||||
|
details = 'Container ' + eventAttr.name + ' detached';
|
||||||
|
break;
|
||||||
|
case 'copy':
|
||||||
|
details = 'Container ' + eventAttr.name + ' copied';
|
||||||
|
break;
|
||||||
|
case 'export':
|
||||||
|
details = 'Container ' + eventAttr.name + ' exported';
|
||||||
|
break;
|
||||||
|
case 'health_status':
|
||||||
|
details = 'Container ' + eventAttr.name + ' executed health status';
|
||||||
|
break;
|
||||||
|
case 'oom':
|
||||||
|
details = 'Container ' + eventAttr.name + ' goes in out of memory';
|
||||||
|
break;
|
||||||
|
case 'rename':
|
||||||
|
details = 'Container ' + eventAttr.name + ' renamed';
|
||||||
|
break;
|
||||||
|
case 'resize':
|
||||||
|
details = 'Container ' + eventAttr.name + ' resized';
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
details = 'Showed running processes for container ' + eventAttr.name;
|
||||||
|
break;
|
||||||
|
case 'update':
|
||||||
|
details = 'Container ' + eventAttr.name + ' updated';
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (event.Action.indexOf('exec_create') === 0) {
|
if (event.Action.indexOf('exec_create') === 0) {
|
||||||
details = 'Exec instance created';
|
details = 'Exec instance created';
|
||||||
|
@ -52,15 +79,27 @@ function createEventDetails(event) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
details = 'Image deleted';
|
details = 'Image deleted';
|
||||||
break;
|
break;
|
||||||
|
case 'import':
|
||||||
|
details = 'Image ' + event.Actor.ID + ' imported';
|
||||||
|
break;
|
||||||
|
case 'load':
|
||||||
|
details = 'Image ' + event.Actor.ID + ' loaded';
|
||||||
|
break;
|
||||||
case 'tag':
|
case 'tag':
|
||||||
details = 'New tag created for ' + eventAttr.name;
|
details = 'New tag created for ' + eventAttr.name;
|
||||||
break;
|
break;
|
||||||
case 'untag':
|
case 'untag':
|
||||||
details = 'Image untagged';
|
details = 'Image untagged';
|
||||||
break;
|
break;
|
||||||
|
case 'save':
|
||||||
|
details = 'Image ' + event.Actor.ID + ' saved';
|
||||||
|
break;
|
||||||
case 'pull':
|
case 'pull':
|
||||||
details = 'Image ' + event.Actor.ID + ' pulled';
|
details = 'Image ' + event.Actor.ID + ' pulled';
|
||||||
break;
|
break;
|
||||||
|
case 'push':
|
||||||
|
details = 'Image ' + event.Actor.ID + ' pushed';
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
details = 'Unsupported event';
|
details = 'Unsupported event';
|
||||||
}
|
}
|
||||||
|
@ -73,6 +112,9 @@ function createEventDetails(event) {
|
||||||
case 'destroy':
|
case 'destroy':
|
||||||
details = 'Network ' + eventAttr.name + ' deleted';
|
details = 'Network ' + eventAttr.name + ' deleted';
|
||||||
break;
|
break;
|
||||||
|
case 'remove':
|
||||||
|
details = 'Network ' + eventAttr.name + ' removed';
|
||||||
|
break;
|
||||||
case 'connect':
|
case 'connect':
|
||||||
details = 'Container connected to ' + eventAttr.name + ' network';
|
details = 'Container connected to ' + eventAttr.name + ' network';
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -8,3 +8,24 @@ function ImageViewModel(data) {
|
||||||
this.VirtualSize = data.VirtualSize;
|
this.VirtualSize = data.VirtualSize;
|
||||||
this.ContainerCount = data.ContainerCount;
|
this.ContainerCount = data.ContainerCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ImageBuildModel(data) {
|
||||||
|
this.hasError = false;
|
||||||
|
var buildLogs = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
var line = data[i];
|
||||||
|
|
||||||
|
if (line.stream) {
|
||||||
|
line = line.stream.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||||
|
buildLogs.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.errorDetail) {
|
||||||
|
buildLogs.push(line.errorDetail.message);
|
||||||
|
this.hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buildLogs = buildLogs;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
function StackTemplateViewModel(data) {
|
function StackTemplateViewModel(data) {
|
||||||
this.Type = data.type;
|
this.Type = data.type;
|
||||||
|
this.Name = data.name;
|
||||||
this.Title = data.title;
|
this.Title = data.title;
|
||||||
this.Description = data.description;
|
this.Description = data.description;
|
||||||
this.Note = data.note;
|
this.Note = data.note;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
function TemplateViewModel(data) {
|
function TemplateViewModel(data) {
|
||||||
this.Type = data.type;
|
this.Type = data.type;
|
||||||
|
this.Name = data.name;
|
||||||
this.Title = data.title;
|
this.Title = data.title;
|
||||||
this.Description = data.description;
|
this.Description = data.description;
|
||||||
this.Note = data.note;
|
this.Note = data.note;
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.factory('Build', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BuildFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/build', {
|
||||||
|
endpointId: EndpointProvider.endpointID
|
||||||
|
},
|
||||||
|
{
|
||||||
|
buildImage: {
|
||||||
|
method: 'POST', ignoreLoadingBar: true,
|
||||||
|
transformResponse: jsonObjectsToArrayHandler, isArray: true,
|
||||||
|
headers: { 'Content-Type': 'application/x-tar' }
|
||||||
|
},
|
||||||
|
buildImageOverride: {
|
||||||
|
method: 'POST', ignoreLoadingBar: true,
|
||||||
|
transformResponse: jsonObjectsToArrayHandler, isArray: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -0,0 +1,10 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.factory('Commit', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function CommitFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/commit', {
|
||||||
|
endpointId: EndpointProvider.endpointID
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commitContainer: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}, ignoreLoadingBar: true}
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -13,6 +13,11 @@ angular.module('portainer.docker')
|
||||||
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
||||||
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
||||||
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
|
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
|
||||||
|
logs: {
|
||||||
|
method: 'GET', params: { id: '@id', action: 'logs' },
|
||||||
|
timeout: 4500, ignoreLoadingBar: true,
|
||||||
|
transformResponse: logsHandler, isArray: true
|
||||||
|
},
|
||||||
stats: {
|
stats: {
|
||||||
method: 'GET', params: { id: '@id', stream: false, action: 'stats' },
|
method: 'GET', params: { id: '@id', stream: false, action: 'stats' },
|
||||||
timeout: 4500, ignoreLoadingBar: true
|
timeout: 4500, ignoreLoadingBar: true
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
angular.module('portainer.docker')
|
|
||||||
.factory('ContainerCommit', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerCommitFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
|
||||||
'use strict';
|
|
||||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/commit', {
|
|
||||||
endpointId: EndpointProvider.endpointID
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}, ignoreLoadingBar: true}
|
|
||||||
});
|
|
||||||
}]);
|
|
|
@ -1,21 +0,0 @@
|
||||||
angular.module('portainer.docker')
|
|
||||||
.factory('ContainerLogs', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerLogsFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
|
||||||
'use strict';
|
|
||||||
return {
|
|
||||||
get: function (id, params, callback) {
|
|
||||||
$http({
|
|
||||||
method: 'GET',
|
|
||||||
url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/containers/' + id + '/logs',
|
|
||||||
params: {
|
|
||||||
'stdout': params.stdout || 0,
|
|
||||||
'stderr': params.stderr || 0,
|
|
||||||
'timestamps': params.timestamps || 0,
|
|
||||||
'tail': params.tail || 'all'
|
|
||||||
},
|
|
||||||
ignoreLoadingBar: true
|
|
||||||
}).success(callback).error(function (data, status, headers, config) {
|
|
||||||
console.log(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}]);
|
|
|
@ -44,6 +44,18 @@ function genericHandler(data) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Docker API returns the logs as a single string.
|
||||||
|
// This handler will return an array with each line being an entry.
|
||||||
|
// It will also strip the 8 first characters of each line and remove any ANSI code related character sequences.
|
||||||
|
function logsHandler(data) {
|
||||||
|
var logs = data;
|
||||||
|
logs = logs.substring(8);
|
||||||
|
logs = logs.replace(/\n(.{8})/g, '\n\r');
|
||||||
|
logs = logs.replace(
|
||||||
|
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||||
|
return logs.split('\n');
|
||||||
|
}
|
||||||
|
|
||||||
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
|
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
|
||||||
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
|
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
|
||||||
// container the error (Docker = 1.12).
|
// container the error (Docker = 1.12).
|
||||||
|
|
|
@ -13,6 +13,11 @@ angular.module('portainer.docker')
|
||||||
ignoreLoadingBar: true
|
ignoreLoadingBar: true
|
||||||
},
|
},
|
||||||
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
|
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
|
||||||
remove: { method: 'DELETE', params: {id: '@id'} }
|
remove: { method: 'DELETE', params: {id: '@id'} },
|
||||||
|
logs: {
|
||||||
|
method: 'GET', params: { id: '@id', action: 'logs' },
|
||||||
|
timeout: 4500, ignoreLoadingBar: true,
|
||||||
|
transformResponse: logsHandler, isArray: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
angular.module('portainer.docker')
|
|
||||||
.factory('ServiceLogs', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ServiceLogsFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
|
||||||
'use strict';
|
|
||||||
return {
|
|
||||||
get: function (id, params, callback) {
|
|
||||||
$http({
|
|
||||||
method: 'GET',
|
|
||||||
url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/services/' + id + '/logs',
|
|
||||||
params: {
|
|
||||||
'stdout': params.stdout || 0,
|
|
||||||
'stderr': params.stderr || 0,
|
|
||||||
'timestamps': params.timestamps || 0,
|
|
||||||
'tail': params.tail || 'all'
|
|
||||||
}
|
|
||||||
}).success(callback).error(function (data, status, headers, config) {
|
|
||||||
console.log(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}]);
|
|
|
@ -7,7 +7,7 @@ angular.module('portainer.docker')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: { method: 'GET', params: { action: 'info' }, ignoreLoadingBar: true },
|
info: { method: 'GET', params: { action: 'info' }, ignoreLoadingBar: true },
|
||||||
version: { method: 'GET', params: { action: 'version' }, ignoreLoadingBar: true },
|
version: { method: 'GET', params: { action: 'version' }, ignoreLoadingBar: true, timeout: 4500 },
|
||||||
events: {
|
events: {
|
||||||
method: 'GET', params: { action: 'events', since: '@since', until: '@until' },
|
method: 'GET', params: { action: 'events', since: '@since', until: '@until' },
|
||||||
isArray: true, transformResponse: jsonObjectsToArrayHandler
|
isArray: true, transformResponse: jsonObjectsToArrayHandler
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Task', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
.factory('Task', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id', {
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id/:action', {
|
||||||
endpointId: EndpointProvider.endpointID
|
endpointId: EndpointProvider.endpointID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
get: { method: 'GET', params: {id: '@id'} },
|
get: { method: 'GET', params: {id: '@id'} },
|
||||||
query: { method: 'GET', isArray: true, params: {filters: '@filters'} }
|
query: { method: 'GET', isArray: true, params: {filters: '@filters'} },
|
||||||
|
logs: {
|
||||||
|
method: 'GET', params: { id: '@id', action: 'logs' },
|
||||||
|
timeout: 4500, ignoreLoadingBar: true,
|
||||||
|
transformResponse: logsHandler, isArray: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.factory('BuildService', ['$q', 'Build', 'FileUploadService', function BuildServiceFactory($q, Build, FileUploadService) {
|
||||||
|
'use strict';
|
||||||
|
var service = {};
|
||||||
|
|
||||||
|
service.buildImageFromUpload = function(names, file, path) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
FileUploadService.buildImage(names, file, path)
|
||||||
|
.then(function success(response) {
|
||||||
|
var model = new ImageBuildModel(response.data);
|
||||||
|
deferred.resolve(model);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.buildImageFromURL = function(names, url, path) {
|
||||||
|
var params = {
|
||||||
|
t: names,
|
||||||
|
remote: url,
|
||||||
|
dockerfile: path
|
||||||
|
};
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Build.buildImage(params, {}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var model = new ImageBuildModel(data);
|
||||||
|
deferred.resolve(model);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.buildImageFromDockerfileContent = function(names, content) {
|
||||||
|
var params = {
|
||||||
|
t: names
|
||||||
|
};
|
||||||
|
var payload = {
|
||||||
|
content: content
|
||||||
|
};
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Build.buildImageOverride(params, payload).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var model = new ImageBuildModel(data);
|
||||||
|
deferred.resolve(model);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}]);
|
|
@ -131,6 +131,18 @@ angular.module('portainer.docker')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.logs = function(id, stdout, stderr, timestamps, tail) {
|
||||||
|
var parameters = {
|
||||||
|
id: id,
|
||||||
|
stdout: stdout || 0,
|
||||||
|
stderr: stderr || 0,
|
||||||
|
timestamps: timestamps || 0,
|
||||||
|
tail: tail || 'all'
|
||||||
|
};
|
||||||
|
|
||||||
|
return Container.logs(parameters).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
service.containerStats = function(id) {
|
service.containerStats = function(id) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
|
|
@ -58,5 +58,17 @@ angular.module('portainer.docker')
|
||||||
return Service.update({ id: service.Id, version: service.Version }, config).$promise;
|
return Service.update({ id: service.Id, version: service.Version }, config).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.logs = function(id, stdout, stderr, timestamps, tail) {
|
||||||
|
var parameters = {
|
||||||
|
id: id,
|
||||||
|
stdout: stdout || 0,
|
||||||
|
stderr: stderr || 0,
|
||||||
|
timestamps: timestamps || 0,
|
||||||
|
tail: tail || 'all'
|
||||||
|
};
|
||||||
|
|
||||||
|
return Service.logs(parameters).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -35,5 +35,17 @@ angular.module('portainer.docker')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.logs = function(id, stdout, stderr, timestamps, tail) {
|
||||||
|
var parameters = {
|
||||||
|
id: id,
|
||||||
|
stdout: stdout || 0,
|
||||||
|
stderr: stderr || 0,
|
||||||
|
timestamps: timestamps || 0,
|
||||||
|
tail: tail || 'all'
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.logs(parameters).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('CreateConfigController', ['$scope', '$state', '$document', 'Notifications', 'ConfigService', 'Authentication', 'FormValidator', 'ResourceControlService', 'CodeMirrorService',
|
.controller('CreateConfigController', ['$scope', '$state', 'Notifications', 'ConfigService', 'Authentication', 'FormValidator', 'ResourceControlService',
|
||||||
function ($scope, $state, $document, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService, CodeMirrorService) {
|
function ($scope, $state, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) {
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Name: '',
|
Name: '',
|
||||||
Labels: [],
|
Labels: [],
|
||||||
AccessControlData: new AccessControlFormData()
|
AccessControlData: new AccessControlFormData(),
|
||||||
|
ConfigContent: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
|
@ -31,9 +32,7 @@ function ($scope, $state, $document, Notifications, ConfigService, Authenticatio
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareConfigData(config) {
|
function prepareConfigData(config) {
|
||||||
// The codemirror editor does not work with ng-model so we need to retrieve
|
var configData = $scope.formValues.ConfigContent;
|
||||||
// the value directly from the editor.
|
|
||||||
var configData = $scope.editor.getValue();
|
|
||||||
config.Data = btoa(unescape(encodeURIComponent(configData)));
|
config.Data = btoa(unescape(encodeURIComponent(configData)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +61,11 @@ function ($scope, $state, $document, Notifications, ConfigService, Authenticatio
|
||||||
var userDetails = Authentication.getUserDetails();
|
var userDetails = Authentication.getUserDetails();
|
||||||
var isAdmin = userDetails.role === 1;
|
var isAdmin = userDetails.role === 1;
|
||||||
|
|
||||||
|
if ($scope.formValues.ConfigContent === '') {
|
||||||
|
$scope.state.formValidationError = 'Config content must not be empty';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!validateForm(accessControlData, isAdmin)) {
|
if (!validateForm(accessControlData, isAdmin)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -83,14 +87,7 @@ function ($scope, $state, $document, Notifications, ConfigService, Authenticatio
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function initView() {
|
$scope.editorUpdate = function(cm) {
|
||||||
$document.ready(function() {
|
$scope.formValues.ConfigContent = cm.getValue();
|
||||||
var webEditorElement = $document[0].getElementById('config-editor', false);
|
};
|
||||||
if (webEditorElement) {
|
|
||||||
$scope.editor = CodeMirrorService.applyCodeMirrorOnElement(webEditorElement, false, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initView();
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -21,7 +21,12 @@
|
||||||
<!-- config-data -->
|
<!-- config-data -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<textarea id="config-editor" class="form-control"></textarea>
|
<code-editor
|
||||||
|
identifier="config-creation-editor"
|
||||||
|
placeholder="Define or paste the content of your config here"
|
||||||
|
yml="false"
|
||||||
|
on-change="editorUpdate"
|
||||||
|
></code-editor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !config-data -->
|
<!-- !config-data -->
|
||||||
|
@ -62,6 +67,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name" ng-click="create()">Create config</button>
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name" ng-click="create()">Create config</button>
|
||||||
|
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !actions -->
|
<!-- !actions -->
|
||||||
|
|
|
@ -70,7 +70,12 @@
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<textarea id="config-editor" ng-model="config.Data" class="form-control"></textarea>
|
<code-editor
|
||||||
|
identifier="config-editor"
|
||||||
|
yml="false"
|
||||||
|
read-only="true"
|
||||||
|
value="config.Data"
|
||||||
|
></code-editor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ConfigController', ['$scope', '$transition$', '$state', '$document', 'ConfigService', 'Notifications', 'CodeMirrorService',
|
.controller('ConfigController', ['$scope', '$transition$', '$state', 'ConfigService', 'Notifications',
|
||||||
function ($scope, $transition$, $state, $document, ConfigService, Notifications, CodeMirrorService) {
|
function ($scope, $transition$, $state, ConfigService, Notifications) {
|
||||||
|
|
||||||
$scope.removeConfig = function removeConfig(configId) {
|
$scope.removeConfig = function removeConfig(configId) {
|
||||||
ConfigService.remove(configId)
|
ConfigService.remove(configId)
|
||||||
|
@ -13,20 +13,10 @@ function ($scope, $transition$, $state, $document, ConfigService, Notifications,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function initEditor() {
|
|
||||||
$document.ready(function() {
|
|
||||||
var webEditorElement = $document[0].getElementById('config-editor');
|
|
||||||
if (webEditorElement) {
|
|
||||||
$scope.editor = CodeMirrorService.applyCodeMirrorOnElement(webEditorElement, false, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
ConfigService.config($transition$.params().id)
|
ConfigService.config($transition$.params().id)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.config = data;
|
$scope.config = data;
|
||||||
initEditor();
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve config details');
|
Notifications.error('Failure', err, 'Unable to retrieve config details');
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<div class="btn-group" role="group" aria-label="...">
|
<div class="btn-group" role="group" aria-label="...">
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.stats({id: container.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i>Stats</a>
|
<a class="btn" type="button" ui-sref="docker.containers.container.stats({id: container.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i>Stats</a>
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.logs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
|
<a class="btn" type="button" ui-sref="docker.containers.container.logs({id: container.Id})"><i class="fa fa-file-text-o space-right" aria-hidden="true"></i>Logs</a>
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.console({id: container.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
|
<a class="btn" type="button" ui-sref="docker.containers.container.console({id: container.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.inspect({id: container.Id})"><i class="fa fa-info-circle space-right" aria-hidden="true"></i>Inspect</a>
|
<a class="btn" type="button" ui-sref="docker.containers.container.inspect({id: container.Id})"><i class="fa fa-info-circle space-right" aria-hidden="true"></i>Inspect</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService',
|
.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Container', 'Commit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService',
|
||||||
function ($q, $scope, $state, $transition$, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService) {
|
function ($q, $scope, $state, $transition$, $filter, Container, Commit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService) {
|
||||||
$scope.activityTime = 0;
|
$scope.activityTime = 0;
|
||||||
$scope.portBindings = [];
|
$scope.portBindings = [];
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ function ($q, $scope, $state, $transition$, $filter, Container, ContainerCommit,
|
||||||
var image = $scope.config.Image;
|
var image = $scope.config.Image;
|
||||||
var registry = $scope.config.Registry;
|
var registry = $scope.config.Registry;
|
||||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL);
|
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL);
|
||||||
ContainerCommit.commit({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
Commit.commitContainer({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||||
update();
|
update();
|
||||||
Notifications.success('Container commited', $transition$.params().id);
|
Notifications.success('Container commited', $transition$.params().id);
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
|
|
|
@ -1,70 +1,71 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container', 'Notifications',
|
.controller('ContainerLogsController', ['$scope', '$transition$', '$interval', 'ContainerService', 'Notifications',
|
||||||
function ($scope, $transition$, $anchorScroll, ContainerLogs, Container, Notifications) {
|
function ($scope, $transition$, $interval, ContainerService, Notifications) {
|
||||||
$scope.state = {};
|
$scope.state = {
|
||||||
$scope.state.displayTimestampsOut = false;
|
refreshRate: 3,
|
||||||
$scope.state.displayTimestampsErr = false;
|
lineCount: 2000
|
||||||
$scope.stdout = '';
|
|
||||||
$scope.stderr = '';
|
|
||||||
$scope.tailLines = 2000;
|
|
||||||
|
|
||||||
Container.get({id: $transition$.params().id}, function (d) {
|
|
||||||
$scope.container = d;
|
|
||||||
}, function (e) {
|
|
||||||
Notifications.error('Failure', e, 'Unable to retrieve container info');
|
|
||||||
});
|
|
||||||
|
|
||||||
function getLogs() {
|
|
||||||
getLogsStdout();
|
|
||||||
getLogsStderr();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLogsStderr() {
|
|
||||||
ContainerLogs.get($transition$.params().id, {
|
|
||||||
stdout: 0,
|
|
||||||
stderr: 1,
|
|
||||||
timestamps: $scope.state.displayTimestampsErr,
|
|
||||||
tail: $scope.tailLines
|
|
||||||
}, function (data, status, headers, config) {
|
|
||||||
// Replace carriage returns with newlines to clean up output
|
|
||||||
data = data.replace(/[\r]/g, '\n');
|
|
||||||
// Strip 8 byte header from each line of output
|
|
||||||
data = data.substring(8);
|
|
||||||
data = data.replace(/\n(.{8})/g, '\n');
|
|
||||||
$scope.stderr = data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLogsStdout() {
|
|
||||||
ContainerLogs.get($transition$.params().id, {
|
|
||||||
stdout: 1,
|
|
||||||
stderr: 0,
|
|
||||||
timestamps: $scope.state.displayTimestampsOut,
|
|
||||||
tail: $scope.tailLines
|
|
||||||
}, function (data, status, headers, config) {
|
|
||||||
// Replace carriage returns with newlines to clean up output
|
|
||||||
data = data.replace(/[\r]/g, '\n');
|
|
||||||
// Strip 8 byte header from each line of output
|
|
||||||
data = data.substring(8);
|
|
||||||
data = data.replace(/\n(.{8})/g, '\n');
|
|
||||||
$scope.stdout = data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// initial call
|
|
||||||
getLogs();
|
|
||||||
var logIntervalId = window.setInterval(getLogs, 5000);
|
|
||||||
|
|
||||||
$scope.$on('$destroy', function () {
|
|
||||||
// clearing interval when view changes
|
|
||||||
clearInterval(logIntervalId);
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.toggleTimestampsOut = function () {
|
|
||||||
getLogsStdout();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.toggleTimestampsErr = function () {
|
$scope.changeLogCollection = function(logCollectionStatus) {
|
||||||
getLogsStderr();
|
if (!logCollectionStatus) {
|
||||||
|
stopRepeater();
|
||||||
|
} else {
|
||||||
|
setUpdateRepeater();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
stopRepeater();
|
||||||
|
});
|
||||||
|
|
||||||
|
function stopRepeater() {
|
||||||
|
var repeater = $scope.repeater;
|
||||||
|
if (angular.isDefined(repeater)) {
|
||||||
|
$interval.cancel(repeater);
|
||||||
|
repeater = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(logs) {
|
||||||
|
$scope.logs = logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpdateRepeater() {
|
||||||
|
var refreshRate = $scope.state.refreshRate;
|
||||||
|
$scope.repeater = $interval(function() {
|
||||||
|
ContainerService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.logs = data;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve container logs');
|
||||||
|
});
|
||||||
|
}, refreshRate * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLogPolling() {
|
||||||
|
ContainerService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.logs = data;
|
||||||
|
setUpdateRepeater();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve container logs');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
ContainerService.container($transition$.params().id)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.container = data;
|
||||||
|
startLogPolling();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve container information');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -5,50 +5,6 @@
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row">
|
<log-viewer
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
data="logs" ng-if="logs" log-collection-change="changeLogCollection"
|
||||||
<rd-widget>
|
></log-viewer>
|
||||||
<rd-widget-body>
|
|
||||||
<div class="widget-icon grey pull-left">
|
|
||||||
<i class="fa fa-server"></i>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ container.Name|trimcontainername }}</div>
|
|
||||||
<div class="comment">Name</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-info-circle" title="Stdout logs"></rd-widget-header>
|
|
||||||
<rd-widget-taskbar>
|
|
||||||
<input type="checkbox" ng-model="state.displayTimestampsOut" id="displayAllTsOut" ng-change="toggleTimestampsOut()"/>
|
|
||||||
<label for="displayAllTsOut">Display timestamps</label>
|
|
||||||
</rd-widget-taskbar>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="panel-body">
|
|
||||||
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-exclamation-triangle" title="Stderr logs"></rd-widget-header>
|
|
||||||
<rd-widget-taskbar>
|
|
||||||
<input type="checkbox" ng-model="state.displayTimestampsErr" id="displayAllTsErr" ng-change="toggleTimestampsErr()"/>
|
|
||||||
<label for="displayAllTsErr">Display timestamps</label>
|
|
||||||
</rd-widget-taskbar>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="panel-body">
|
|
||||||
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.controller('BuildImageController', ['$scope', '$state', 'BuildService', 'Notifications',
|
||||||
|
function ($scope, $state, BuildService, Notifications) {
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
BuildType: 'editor',
|
||||||
|
actionInProgress: false,
|
||||||
|
activeTab: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.formValues = {
|
||||||
|
ImageNames: [{ Name: '' }],
|
||||||
|
UploadFile: null,
|
||||||
|
DockerFileContent: '',
|
||||||
|
URL: '',
|
||||||
|
Path: 'Dockerfile'
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addImageName = function() {
|
||||||
|
$scope.formValues.ImageNames.push({ Name: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removeImageName = function(index) {
|
||||||
|
$scope.formValues.ImageNames.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildImageBasedOnBuildType(method, names) {
|
||||||
|
var buildType = $scope.state.BuildType;
|
||||||
|
var dockerfilePath = $scope.formValues.Path;
|
||||||
|
|
||||||
|
if (buildType === 'upload') {
|
||||||
|
var file = $scope.formValues.UploadFile;
|
||||||
|
return BuildService.buildImageFromUpload(names, file, dockerfilePath);
|
||||||
|
} else if (buildType === 'url') {
|
||||||
|
var URL = $scope.formValues.URL;
|
||||||
|
return BuildService.buildImageFromURL(names, URL, dockerfilePath);
|
||||||
|
} else {
|
||||||
|
var dockerfileContent = $scope.formValues.DockerFileContent;
|
||||||
|
return BuildService.buildImageFromDockerfileContent(names, dockerfileContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.buildImage = function() {
|
||||||
|
var buildType = $scope.state.BuildType;
|
||||||
|
|
||||||
|
if (buildType === 'editor' && $scope.formValues.DockerFileContent === '') {
|
||||||
|
$scope.state.formValidationError = 'Dockerfile content must not be empty';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.state.actionInProgress = true;
|
||||||
|
|
||||||
|
var imageNames = $scope.formValues.ImageNames.filter(function filterNull(x) {
|
||||||
|
return x.Name;
|
||||||
|
}).map(function getNames(x) {
|
||||||
|
return x.Name;
|
||||||
|
});
|
||||||
|
|
||||||
|
buildImageBasedOnBuildType(buildType, imageNames)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.buildLogs = data.buildLogs;
|
||||||
|
$scope.state.activeTab = 1;
|
||||||
|
if (data.hasError) {
|
||||||
|
Notifications.error('An error occured during build', { msg: 'Please check build logs output' });
|
||||||
|
} else {
|
||||||
|
Notifications.success('Image successfully built');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to build image');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$scope.state.actionInProgress = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.validImageNames = function() {
|
||||||
|
for (var i = 0; i < $scope.formValues.ImageNames.length; i++) {
|
||||||
|
var item = $scope.formValues.ImageNames[i];
|
||||||
|
if (item.Name !== '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.editorUpdate = function(cm) {
|
||||||
|
$scope.formValues.DockerFileContent = cm.getValue();
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -0,0 +1,231 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Build image"></rd-header-title>
|
||||||
|
<rd-header-content>
|
||||||
|
<a ui-sref="docker.images">Images</a> > Build image
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body>
|
||||||
|
<uib-tabset active="state.activeTab">
|
||||||
|
<uib-tab index="0">
|
||||||
|
<uib-tab-heading>
|
||||||
|
<i class="fa fa-wrench space-right" aria-hidden="true"></i> Builder
|
||||||
|
</uib-tab-heading>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Naming
|
||||||
|
</div>
|
||||||
|
<!-- names -->
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
You can specify multiple names to your image.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label class="control-label text-left">Names</label>
|
||||||
|
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addImageName()">
|
||||||
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> add additional name
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !names -->
|
||||||
|
<div class="form-group" ng-if="formValues.ImageNames.length === 0">
|
||||||
|
<span class="col-sm-12 text-danger small">
|
||||||
|
<i class="fa fa-exclamation-triangle space-right" aria-hidden="true"></i>You must specify at least one name for the image.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- name-input-list -->
|
||||||
|
<div ng-if="formValues.ImageNames.length > 0">
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
A name must be specified in one of the following formats: <code>name:tag</code>, <code>repository/name:tag</code> or <code>registryfqdn:port/repository/name:tag</code> format. If you omit the tag the default <b>latest</b> value is assumed.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" >
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||||
|
<div ng-repeat="item in formValues.ImageNames track by $index" style="margin-top: 2px;">
|
||||||
|
<!-- name-input -->
|
||||||
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
|
<span class="input-group-addon">name</span>
|
||||||
|
<input type="text" class="form-control" ng-model="item.Name" placeholder="e.g. myImage:myTag" auto-focus>
|
||||||
|
<span class="input-group-addon"><i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[item.Name !== '']" aria-hidden="true"></i></span>
|
||||||
|
</div>
|
||||||
|
<!-- !name-input -->
|
||||||
|
<!-- actions -->
|
||||||
|
<div class="input-group col-sm-2 input-group-sm">
|
||||||
|
<button class="btn btn-sm btn-danger" type="button" ng-click="removeImageName($index)">
|
||||||
|
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !name-input-list -->
|
||||||
|
<!-- build-method -->
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Build method
|
||||||
|
</div>
|
||||||
|
<div class="form-group"></div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0">
|
||||||
|
<div class="boxselector_wrapper">
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="method_editor" ng-model="state.BuildType" value="editor" ng-click="toggleEditor()">
|
||||||
|
<label for="method_editor">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Web editor
|
||||||
|
</div>
|
||||||
|
<p>Use our Web editor</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="method_upload" ng-model="state.BuildType" value="upload" ng-click="saveEditorContent()">
|
||||||
|
<label for="method_upload">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Upload
|
||||||
|
</div>
|
||||||
|
<p>Upload a tarball or a Dockerfile from your computer</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="method_url" ng-model="state.BuildType" value="url" ng-click="saveEditorContent()">
|
||||||
|
<label for="method_url">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-globe" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
URL
|
||||||
|
</div>
|
||||||
|
<p>Specify an URL to a file</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !build-method -->
|
||||||
|
<!-- web-editor -->
|
||||||
|
<div ng-show="state.BuildType === 'editor'">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Web editor
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
You can get more information about Dockerfile format in the <a href="https://docs.docker.com/engine/reference/builder/" target="_blank">official documentation</a>.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<code-editor
|
||||||
|
identifier="image-build-editor"
|
||||||
|
placeholder="Define or paste the content of your Dockerfile here"
|
||||||
|
yml="false"
|
||||||
|
on-change="editorUpdate"
|
||||||
|
></code-editor>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !web-editor -->
|
||||||
|
<!-- upload -->
|
||||||
|
<div ng-show="state.BuildType === 'upload'">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Upload
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
You can upload a Dockerfile or a tar archive containing a Dockerfile from your computer. When using an tarball, the root folder will be used as the build context.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.UploadFile">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
{{ formValues.UploadFile.name }}
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.UploadFile" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ng-if="formValues.UploadFile.type === 'application/gzip' || formValues.UploadFile.type === 'application/x-tar'">
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
Indicate the path to the Dockerfile within the tarball.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="image_path" class="col-sm-2 control-label text-left">Dockerfile path</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" ng-model="formValues.Path" id="image_path" placeholder="Dockerfile">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !upload -->
|
||||||
|
<!-- url -->
|
||||||
|
<div ng-show="state.BuildType === 'url'">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
URL
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
Specify the URL to a Dockerfile, a tarball or a public Git repository (suffixed by <b>.git</b>). When using a tarball or a Git repository URL, the root folder will be used as the build context.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="image_url" class="col-sm-2 control-label text-left">URL</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" ng-model="formValues.URL" id="image_url" placeholder="https://myhost.mydomain/myimage.tar.gz">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
Indicate the path to the Dockerfile within the tarball/repository (ignored when using a Dockerfile).
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="image_path" class="col-sm-2 control-label text-left">Dockerfile path</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" ng-model="formValues.Path" id="image_path" placeholder="Dockerfile">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !url -->
|
||||||
|
<!-- 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
|
||||||
|
|| (state.BuildType === 'upload' && (!formValues.UploadFile || !formValues.Path))
|
||||||
|
|| (state.BuildType === 'url' && (!formValues.URL || !formValues.Path))
|
||||||
|
|| (formValues.ImageNames.length === 0 || !validImageNames())"
|
||||||
|
ng-click="buildImage()" button-spinner="state.actionInProgress">
|
||||||
|
<span ng-hide="state.actionInProgress">Build the image</span>
|
||||||
|
<span ng-show="state.actionInProgress">Image building in progress...</span>
|
||||||
|
</button>
|
||||||
|
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
|
</form>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab index="1" disable="!buildLogs">
|
||||||
|
<uib-tab-heading>
|
||||||
|
<i class="fa fa-file-text space-right" aria-hidden="true"></i> Output
|
||||||
|
</uib-tab-heading>
|
||||||
|
<pre class="log_viewer">
|
||||||
|
<div ng-repeat="line in buildLogs track by $index" class="line"><p class="inner_line" ng-click="active=!active" ng-class="{'line_selected': active}">{{ line }}</p></div>
|
||||||
|
<div ng-if="!buildLogs.length" class="line"><p class="inner_line">No build output available.</p></div>
|
||||||
|
</pre>
|
||||||
|
</uib-tab>
|
||||||
|
</uib-tabset>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -6,5 +6,6 @@
|
||||||
nodes="nodes"
|
nodes="nodes"
|
||||||
show-text-filter="true"
|
show-text-filter="true"
|
||||||
show-slot-column="service.Mode !== 'global'"
|
show-slot-column="service.Mode !== 'global'"
|
||||||
|
show-logs-button="applicationState.endpoint.apiVersion >= 1.30"
|
||||||
></tasks-datatable>
|
></tasks-datatable>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<a ng-if="applicationState.endpoint.apiVersion >= 1.30" class="btn btn-primary btn-sm" type="button" ui-sref="docker.services.service.logs({id: service.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Service logs</a>
|
<a ng-if="applicationState.endpoint.apiVersion >= 1.30" class="btn btn-primary btn-sm" type="button" ui-sref="docker.services.service.logs({id: service.Id})"><i class="fa fa-file-text-o space-right" aria-hidden="true"></i>Service logs</a>
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || isUpdating" ng-click="forceUpdateService(service)" button-spinner="state.updateInProgress" ng-if="applicationState.endpoint.apiVersion >= 1.25">
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || isUpdating" ng-click="forceUpdateService(service)" button-spinner="state.updateInProgress" ng-if="applicationState.endpoint.apiVersion >= 1.25">
|
||||||
<span ng-hide="state.updateInProgress"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Update the service</span>
|
<span ng-hide="state.updateInProgress"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Update the service</span>
|
||||||
<span ng-show="state.updateInProgress">Update in progress...</span>
|
<span ng-show="state.updateInProgress">Update in progress...</span>
|
||||||
|
|
|
@ -1,78 +1,69 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ServiceLogsController', ['$scope', '$transition$', '$anchorScroll', 'ServiceLogs', 'Service',
|
.controller('ServiceLogsController', ['$scope', '$transition$', '$interval', 'ServiceService', 'Notifications',
|
||||||
function ($scope, $transition$, $anchorScroll, ServiceLogs, Service) {
|
function ($scope, $transition$, $interval, ServiceService, Notifications) {
|
||||||
$scope.state = {};
|
$scope.state = {
|
||||||
$scope.state.displayTimestampsOut = false;
|
refreshRate: 3,
|
||||||
$scope.state.displayTimestampsErr = false;
|
lineCount: 2000
|
||||||
$scope.stdout = '';
|
};
|
||||||
$scope.stderr = '';
|
|
||||||
$scope.tailLines = 2000;
|
|
||||||
|
|
||||||
function getLogs() {
|
$scope.changeLogCollection = function(logCollectionStatus) {
|
||||||
getLogsStdout();
|
if (!logCollectionStatus) {
|
||||||
getLogsStderr();
|
stopRepeater();
|
||||||
|
} else {
|
||||||
|
setUpdateRepeater();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function getLogsStderr() {
|
$scope.$on('$destroy', function() {
|
||||||
ServiceLogs.get($transition$.params().id, {
|
stopRepeater();
|
||||||
stdout: 0,
|
|
||||||
stderr: 1,
|
|
||||||
timestamps: $scope.state.displayTimestampsErr,
|
|
||||||
tail: $scope.tailLines
|
|
||||||
}, function (data, status, headers, config) {
|
|
||||||
// Replace carriage returns with newlines to clean up output
|
|
||||||
data = data.replace(/[\r]/g, '\n');
|
|
||||||
// Strip 8 byte header from each line of output
|
|
||||||
data = data.substring(8);
|
|
||||||
data = data.replace(/\n(.{8})/g, '\n');
|
|
||||||
$scope.stderr = data;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function stopRepeater() {
|
||||||
|
var repeater = $scope.repeater;
|
||||||
|
if (angular.isDefined(repeater)) {
|
||||||
|
$interval.cancel(repeater);
|
||||||
|
repeater = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLogsStdout() {
|
function setUpdateRepeater() {
|
||||||
ServiceLogs.get($transition$.params().id, {
|
var refreshRate = $scope.state.refreshRate;
|
||||||
stdout: 1,
|
$scope.repeater = $interval(function() {
|
||||||
stderr: 0,
|
ServiceService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
timestamps: $scope.state.displayTimestampsOut,
|
.then(function success(data) {
|
||||||
tail: $scope.tailLines
|
$scope.logs = data;
|
||||||
}, function (data, status, headers, config) {
|
})
|
||||||
// Replace carriage returns with newlines to clean up output
|
.catch(function error(err) {
|
||||||
data = data.replace(/[\r]/g, '\n');
|
stopRepeater();
|
||||||
// Strip 8 byte header from each line of output
|
Notifications.error('Failure', err, 'Unable to retrieve service logs');
|
||||||
data = data.substring(8);
|
|
||||||
data = data.replace(/\n(.{8})/g, '\n');
|
|
||||||
$scope.stdout = data;
|
|
||||||
});
|
});
|
||||||
|
}, refreshRate * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getService() {
|
function startLogPolling() {
|
||||||
Service.get({id: $transition$.params().id}, function (d) {
|
ServiceService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
$scope.service = d;
|
.then(function success(data) {
|
||||||
}, function (e) {
|
$scope.logs = data;
|
||||||
Notifications.error('Failure', e, 'Unable to retrieve service info');
|
console.log(JSON.stringify(data, null, 4));
|
||||||
|
setUpdateRepeater();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve service logs');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
getService();
|
ServiceService.service($transition$.params().id)
|
||||||
getLogs();
|
.then(function success(data) {
|
||||||
|
$scope.service = data;
|
||||||
var logIntervalId = window.setInterval(getLogs, 5000);
|
startLogPolling();
|
||||||
|
})
|
||||||
$scope.$on('$destroy', function () {
|
.catch(function error(err) {
|
||||||
// clearing interval when view changes
|
Notifications.error('Failure', err, 'Unable to retrieve service information');
|
||||||
clearInterval(logIntervalId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.toggleTimestampsOut = function () {
|
|
||||||
getLogsStdout();
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.toggleTimestampsErr = function () {
|
|
||||||
getLogsStderr();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,54 +1,10 @@
|
||||||
<rd-header>
|
<rd-header>
|
||||||
<rd-header-title title="Service logs"></rd-header-title>
|
<rd-header-title title="Service logs"></rd-header-title>
|
||||||
<rd-header-content>
|
<rd-header-content>
|
||||||
<a ui-sref="docker.services">Services</a> > <a ui-sref="docker.services.service({id: service.ID})">{{ service.Spec.Name }}</a> > Logs
|
<a ui-sref="docker.services">Services</a> > <a ui-sref="docker.services.service({id: service.ID})">{{ service.Name }}</a> > Logs
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row">
|
<log-viewer
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
data="logs" ng-if="logs" log-collection-change="changeLogCollection"
|
||||||
<rd-widget>
|
></log-viewer>
|
||||||
<rd-widget-body>
|
|
||||||
<div class="widget-icon grey pull-left">
|
|
||||||
<i class="fa fa-list-alt"></i>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ service.Spec.Name }}</div>
|
|
||||||
<div class="comment">Name</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-info-circle" title="Stdout logs"></rd-widget-header>
|
|
||||||
<rd-widget-taskbar>
|
|
||||||
<input type="checkbox" ng-model="state.displayTimestampsOut" id="displayAllTsOut" ng-change="toggleTimestampsOut()"/>
|
|
||||||
<label for="displayAllTsOut">Display timestamps</label>
|
|
||||||
</rd-widget-taskbar>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="panel-body">
|
|
||||||
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-exclamation-triangle" title="Stderr logs"></rd-widget-header>
|
|
||||||
<rd-widget-taskbar>
|
|
||||||
<input type="checkbox" ng-model="state.displayTimestampsErr" id="displayAllTsErr" ng-change="toggleTimestampsErr()"/>
|
|
||||||
<label for="displayAllTsErr">Display timestamps</label>
|
|
||||||
</rd-widget-taskbar>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="panel-body">
|
|
||||||
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
remove-action="removeAction"
|
remove-action="removeAction"
|
||||||
scale-action="scaleAction"
|
scale-action="scaleAction"
|
||||||
force-update-action="forceUpdateAction"
|
force-update-action="forceUpdateAction"
|
||||||
swarm-manager-ip="swarmManagerIP"
|
public-url="state.publicURL"
|
||||||
show-force-update-button="applicationState.endpoint.apiVersion >= 1.25"
|
show-force-update-button="applicationState.endpoint.apiVersion >= 1.25"
|
||||||
></services-datatable>
|
></services-datatable>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ServicesController', ['$q', '$scope', '$state', 'Service', 'ServiceService', 'ServiceHelper', 'Notifications', 'Task', 'Node', 'NodeHelper', 'ModalService',
|
.controller('ServicesController', ['$q', '$scope', '$state', 'Service', 'ServiceService', 'ServiceHelper', 'Notifications', 'Task', 'Node', 'ModalService', 'EndpointProvider',
|
||||||
function ($q, $scope, $state, Service, ServiceService, ServiceHelper, Notifications, Task, Node, NodeHelper, ModalService) {
|
function ($q, $scope, $state, Service, ServiceService, ServiceHelper, Notifications, Task, Node, ModalService, EndpointProvider) {
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
publicURL: EndpointProvider.endpointPublicURL()
|
||||||
|
};
|
||||||
|
|
||||||
$scope.scaleAction = function scaleService(service) {
|
$scope.scaleAction = function scaleService(service) {
|
||||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||||
|
@ -88,7 +92,6 @@ function ($q, $scope, $state, Service, ServiceService, ServiceHelper, Notificati
|
||||||
nodes: Node.query({}).$promise
|
nodes: Node.query({}).$promise
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.swarmManagerIP = NodeHelper.getManagerIP(data.nodes);
|
|
||||||
$scope.services = data.services.map(function (service) {
|
$scope.services = data.services.map(function (service) {
|
||||||
var runningTasks = data.tasks.filter(function (task) {
|
var runningTasks = data.tasks.filter(function (task) {
|
||||||
return task.ServiceID === service.ID && task.Status.State === 'running';
|
return task.ServiceID === service.ID && task.Status.State === 'running';
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('CreateStackController', ['$scope', '$state', '$document', 'StackService', 'CodeMirrorService', 'Authentication', 'Notifications', 'FormValidator', 'ResourceControlService', 'FormHelper',
|
.controller('CreateStackController', ['$scope', '$state', 'StackService', 'Authentication', 'Notifications', 'FormValidator', 'ResourceControlService', 'FormHelper',
|
||||||
function ($scope, $state, $document, StackService, CodeMirrorService, Authentication, Notifications, FormValidator, ResourceControlService, FormHelper) {
|
function ($scope, $state, StackService, Authentication, Notifications, FormValidator, ResourceControlService, FormHelper) {
|
||||||
|
|
||||||
// Store the editor content when switching builder methods
|
|
||||||
var editorContent = '';
|
|
||||||
var editorEnabled = true;
|
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Name: '',
|
Name: '',
|
||||||
StackFileContent: '# Define or paste the content of your docker-compose file here',
|
StackFileContent: '',
|
||||||
StackFile: null,
|
StackFile: null,
|
||||||
RepositoryURL: '',
|
RepositoryURL: '',
|
||||||
Env: [],
|
Env: [],
|
||||||
|
@ -42,15 +38,11 @@ function ($scope, $state, $document, StackService, CodeMirrorService, Authentica
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStack(name) {
|
function createStack(name, method) {
|
||||||
var method = $scope.state.Method;
|
|
||||||
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
||||||
|
|
||||||
if (method === 'editor') {
|
if (method === 'editor') {
|
||||||
// The codemirror editor does not work with ng-model so we need to retrieve
|
var stackFileContent = $scope.formValues.StackFileContent;
|
||||||
// the value directly from the editor.
|
|
||||||
var stackFileContent = $scope.editor.getValue();
|
|
||||||
|
|
||||||
return StackService.createStackFromFileContent(name, stackFileContent, env);
|
return StackService.createStackFromFileContent(name, stackFileContent, env);
|
||||||
} else if (method === 'upload') {
|
} else if (method === 'upload') {
|
||||||
var stackFile = $scope.formValues.StackFile;
|
var stackFile = $scope.formValues.StackFile;
|
||||||
|
@ -64,18 +56,24 @@ function ($scope, $state, $document, StackService, CodeMirrorService, Authentica
|
||||||
|
|
||||||
$scope.deployStack = function () {
|
$scope.deployStack = function () {
|
||||||
var name = $scope.formValues.Name;
|
var name = $scope.formValues.Name;
|
||||||
|
var method = $scope.state.Method;
|
||||||
|
|
||||||
var accessControlData = $scope.formValues.AccessControlData;
|
var accessControlData = $scope.formValues.AccessControlData;
|
||||||
var userDetails = Authentication.getUserDetails();
|
var userDetails = Authentication.getUserDetails();
|
||||||
var isAdmin = userDetails.role === 1;
|
var isAdmin = userDetails.role === 1;
|
||||||
var userId = userDetails.ID;
|
var userId = userDetails.ID;
|
||||||
|
|
||||||
|
if (method === 'editor' && $scope.formValues.StackFileContent === '') {
|
||||||
|
$scope.state.formValidationError = 'Stack file content must not be empty';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!validateForm(accessControlData, isAdmin)) {
|
if (!validateForm(accessControlData, isAdmin)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
createStack(name)
|
createStack(name, method)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
Notifications.success('Stack successfully deployed');
|
Notifications.success('Stack successfully deployed');
|
||||||
})
|
})
|
||||||
|
@ -96,33 +94,7 @@ function ($scope, $state, $document, StackService, CodeMirrorService, Authentica
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function enableEditor(value) {
|
$scope.editorUpdate = function(cm) {
|
||||||
$document.ready(function() {
|
$scope.formValues.StackFileContent = cm.getValue();
|
||||||
var webEditorElement = $document[0].getElementById('web-editor');
|
|
||||||
if (webEditorElement) {
|
|
||||||
$scope.editor = CodeMirrorService.applyCodeMirrorOnElement(webEditorElement, true, false);
|
|
||||||
if (value) {
|
|
||||||
$scope.editor.setValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.toggleEditor = function() {
|
|
||||||
if (!editorEnabled) {
|
|
||||||
enableEditor(editorContent);
|
|
||||||
editorEnabled = true;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.saveEditorContent = function() {
|
|
||||||
editorContent = $scope.editor.getValue();
|
|
||||||
editorEnabled = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
function initView() {
|
|
||||||
enableEditor();
|
|
||||||
}
|
|
||||||
|
|
||||||
initView();
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<div class="form-group" style="margin-bottom: 0">
|
<div class="form-group" style="margin-bottom: 0">
|
||||||
<div class="boxselector_wrapper">
|
<div class="boxselector_wrapper">
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="method_editor" ng-model="state.Method" value="editor" ng-click="toggleEditor(state.Method)">
|
<input type="radio" id="method_editor" ng-model="state.Method" value="editor">
|
||||||
<label for="method_editor">
|
<label for="method_editor">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="method_upload" ng-model="state.Method" value="upload" ng-click="saveEditorContent()">
|
<input type="radio" id="method_upload" ng-model="state.Method" value="upload">
|
||||||
<label for="method_upload">
|
<label for="method_upload">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="method_repository" ng-model="state.Method" value="repository" ng-click="saveEditorContent()">
|
<input type="radio" id="method_repository" ng-model="state.Method" value="repository">
|
||||||
<label for="method_repository">
|
<label for="method_repository">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i class="fa fa-git" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-git" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !build-method -->
|
<!-- !build-method -->
|
||||||
<!-- web-editor -->
|
<!-- web-editor -->
|
||||||
<div ng-if="state.Method === 'editor'">
|
<div ng-show="state.Method === 'editor'">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Web editor
|
Web editor
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,13 +75,18 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<textarea id="web-editor" class="form-control" ng-model="formValues.StackFileContent" placeholder='version: "3"'></textarea>
|
<code-editor
|
||||||
|
identifier="stack-creation-editor"
|
||||||
|
placeholder="# Define or paste the content of your docker-compose file here"
|
||||||
|
yml="true"
|
||||||
|
on-change="editorUpdate"
|
||||||
|
></code-editor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !web-editor -->
|
<!-- !web-editor -->
|
||||||
<!-- upload -->
|
<!-- upload -->
|
||||||
<div ng-if="state.Method === 'upload'">
|
<div ng-show="state.Method === 'upload'">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Upload
|
Upload
|
||||||
</div>
|
</div>
|
||||||
|
@ -102,7 +107,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !upload -->
|
<!-- !upload -->
|
||||||
<!-- repository -->
|
<!-- repository -->
|
||||||
<div ng-if="state.Method === 'repository'">
|
<div ng-show="state.Method === 'repository'">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Git repository
|
Git repository
|
||||||
</div>
|
</div>
|
||||||
|
@ -167,7 +172,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || (state.Method === 'editor' && !formValues.StackFileContent)
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress
|
||||||
|| (state.Method === 'upload' && !formValues.StackFile)
|
|| (state.Method === 'upload' && !formValues.StackFile)
|
||||||
|| (state.Method === 'repository' && (!formValues.RepositoryURL || !formValues.RepositoryPath))
|
|| (state.Method === 'repository' && (!formValues.RepositoryURL || !formValues.RepositoryPath))
|
||||||
|| !formValues.Name" ng-click="deployStack()" button-spinner="state.actionInProgress">
|
|| !formValues.Name" ng-click="deployStack()" button-spinner="state.actionInProgress">
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
nodes="nodes"
|
nodes="nodes"
|
||||||
show-text-filter="true"
|
show-text-filter="true"
|
||||||
show-slot-column="true"
|
show-slot-column="true"
|
||||||
|
show-logs-button="applicationState.endpoint.apiVersion >= 1.30"
|
||||||
></tasks-datatable>
|
></tasks-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,7 +58,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<textarea id="web-editor" class="form-control" ng-model="stackFileContent" placeholder='version: "3"'></textarea>
|
<code-editor
|
||||||
|
identifier="stack-editor"
|
||||||
|
placeholder="# Define or paste the content of your docker-compose file here"
|
||||||
|
yml="true"
|
||||||
|
on-change="editorUpdate"
|
||||||
|
value="stackFileContent"
|
||||||
|
></code-editor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('StackController', ['$q', '$scope', '$state', '$transition$', '$document', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ServiceHelper', 'CodeMirrorService', 'Notifications', 'FormHelper', 'EndpointProvider',
|
.controller('StackController', ['$q', '$scope', '$state', '$transition$', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ServiceHelper', 'Notifications', 'FormHelper', 'EndpointProvider',
|
||||||
function ($q, $scope, $state, $transition$, $document, StackService, NodeService, ServiceService, TaskService, ServiceHelper, CodeMirrorService, Notifications, FormHelper, EndpointProvider) {
|
function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceService, TaskService, ServiceHelper, Notifications, FormHelper, EndpointProvider) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
|
@ -12,9 +12,7 @@ function ($q, $scope, $state, $transition$, $document, StackService, NodeService
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deployStack = function () {
|
$scope.deployStack = function () {
|
||||||
// The codemirror editor does not work with ng-model so we need to retrieve
|
var stackFile = $scope.stackFileContent;
|
||||||
// the value directly from the editor.
|
|
||||||
var stackFile = $scope.editor.getValue();
|
|
||||||
var env = FormHelper.removeInvalidEnvVars($scope.stack.Env);
|
var env = FormHelper.removeInvalidEnvVars($scope.stack.Env);
|
||||||
var prune = $scope.formValues.Prune;
|
var prune = $scope.formValues.Prune;
|
||||||
|
|
||||||
|
@ -63,13 +61,6 @@ function ($q, $scope, $state, $transition$, $document, StackService, NodeService
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.stackFileContent = data.stackFile;
|
$scope.stackFileContent = data.stackFile;
|
||||||
|
|
||||||
$document.ready(function() {
|
|
||||||
var webEditorElement = $document[0].getElementById('web-editor');
|
|
||||||
if (webEditorElement) {
|
|
||||||
$scope.editor = CodeMirrorService.applyCodeMirrorOnElement(webEditorElement, true, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.nodes = data.nodes;
|
$scope.nodes = data.nodes;
|
||||||
|
|
||||||
var services = data.services;
|
var services = data.services;
|
||||||
|
@ -89,5 +80,9 @@ function ($q, $scope, $state, $transition$, $document, StackService, NodeService
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.editorUpdate = function(cm) {
|
||||||
|
$scope.stackFileContent = cm.getValue();
|
||||||
|
};
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -52,7 +52,7 @@ function ($q, $scope, $document, $interval, NodeService, ServiceService, TaskSer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function assignServiceName(services, tasks) {
|
function assignServiceInfo(services, tasks) {
|
||||||
for (var i = 0; i < services.length; i++) {
|
for (var i = 0; i < services.length; i++) {
|
||||||
var service = services[i];
|
var service = services[i];
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ function ($q, $scope, $document, $interval, NodeService, ServiceService, TaskSer
|
||||||
function prepareVisualizerData(nodes, services, tasks) {
|
function prepareVisualizerData(nodes, services, tasks) {
|
||||||
var visualizerData = {};
|
var visualizerData = {};
|
||||||
|
|
||||||
assignServiceName(services, tasks);
|
assignServiceInfo(services, tasks);
|
||||||
assignTasksToNode(nodes, tasks);
|
assignTasksToNode(nodes, tasks);
|
||||||
|
|
||||||
visualizerData.nodes = nodes;
|
visualizerData.nodes = nodes;
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
<div><span class="label label-{{ node.Status | nodestatusbadge }}">{{ node.Status }}</span></div>
|
<div><span class="label label-{{ node.Status | nodestatusbadge }}">{{ node.Status }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tasks">
|
<div class="tasks">
|
||||||
<div class="task task_{{ task.Status.State | visualizerTask }}" ng-repeat="task in node.Tasks | filter: (state.DisplayOnlyRunningTasks || '') && { Status: { State: 'running' } }">
|
<div class="task task_{{ task.Status.State | visualizerTask }}" style="border: 2px solid {{ task.ServiceId | visualizerTaskBorderColor }}" ng-repeat="task in node.Tasks | filter: (state.DisplayOnlyRunningTasks || '') && { Status: { State: 'running' } }">
|
||||||
<div class="service_name">{{ task.ServiceName }}</div>
|
<div class="service_name">{{ task.ServiceName }}</div>
|
||||||
<div>Image: {{ task.Spec.ContainerSpec.Image | hideshasum }}</div>
|
<div>Image: {{ task.Spec.ContainerSpec.Image | hideshasum }}</div>
|
||||||
<div>Status: {{ task.Status.State }}</div>
|
<div>Status: {{ task.Status.State }}</div>
|
||||||
|
|
|
@ -44,6 +44,9 @@
|
||||||
<td>Container ID</td>
|
<td>Container ID</td>
|
||||||
<td>{{ task.Status.ContainerStatus.ContainerID }}</td>
|
<td>{{ task.Status.ContainerStatus.ContainerID }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr ng-if="applicationState.endpoint.apiVersion >= 1.30" >
|
||||||
|
<td colspan="2"><a class="btn btn-primary btn-sm" type="button" ui-sref="docker.tasks.task.logs({id: task.Id})"><i class="fa fa-file-text-o space-right" aria-hidden="true"></i>Task logs</a></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('TaskController', ['$scope', '$transition$', 'TaskService', 'Service', 'Notifications',
|
.controller('TaskController', ['$scope', '$transition$', 'TaskService', 'ServiceService', 'Notifications',
|
||||||
function ($scope, $transition$, TaskService, Service, Notifications) {
|
function ($scope, $transition$, TaskService, ServiceService, Notifications) {
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
TaskService.task($transition$.params().id)
|
TaskService.task($transition$.params().id)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var task = data;
|
var task = data;
|
||||||
$scope.task = task;
|
$scope.task = task;
|
||||||
return Service.get({ id: task.ServiceId }).$promise;
|
return ServiceService.service(task.ServiceId);
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var service = new ServiceViewModel(data);
|
var service = data;
|
||||||
$scope.service = service;
|
$scope.service = service;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.controller('TaskLogsController', ['$scope', '$transition$', '$interval', 'TaskService', 'ServiceService', 'Notifications',
|
||||||
|
function ($scope, $transition$, $interval, TaskService, ServiceService, Notifications) {
|
||||||
|
$scope.state = {
|
||||||
|
refreshRate: 3,
|
||||||
|
lineCount: 2000
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.changeLogCollection = function(logCollectionStatus) {
|
||||||
|
if (!logCollectionStatus) {
|
||||||
|
stopRepeater();
|
||||||
|
} else {
|
||||||
|
setUpdateRepeater();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
stopRepeater();
|
||||||
|
});
|
||||||
|
|
||||||
|
function stopRepeater() {
|
||||||
|
var repeater = $scope.repeater;
|
||||||
|
if (angular.isDefined(repeater)) {
|
||||||
|
$interval.cancel(repeater);
|
||||||
|
repeater = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpdateRepeater() {
|
||||||
|
var refreshRate = $scope.state.refreshRate;
|
||||||
|
$scope.repeater = $interval(function() {
|
||||||
|
TaskService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.logs = data;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve task logs');
|
||||||
|
});
|
||||||
|
}, refreshRate * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLogPolling() {
|
||||||
|
TaskService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.logs = data;
|
||||||
|
setUpdateRepeater();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve task logs');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
TaskService.task($transition$.params().id)
|
||||||
|
.then(function success(data) {
|
||||||
|
var task = data;
|
||||||
|
$scope.task = task;
|
||||||
|
return ServiceService.service(task.ServiceId);
|
||||||
|
})
|
||||||
|
.then(function success(data) {
|
||||||
|
var service = data;
|
||||||
|
$scope.service = service;
|
||||||
|
startLogPolling();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve task details');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
|
}]);
|
|
@ -0,0 +1,10 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Task details"></rd-header-title>
|
||||||
|
<rd-header-content ng-if="task && service">
|
||||||
|
<a ui-sref="docker.services">Services</a> > <a ui-sref="docker.services.service({id: service.Id })">{{ service.Name }}</a> > <a ui-sref="docker.tasks.task({id: task.Id })">{{ task.Id }}</a> > Logs
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<log-viewer
|
||||||
|
data="logs" ng-if="logs" log-collection-change="changeLogCollection"
|
||||||
|
></log-viewer>
|
|
@ -178,6 +178,12 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
|
||||||
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === 'bridge'; });
|
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === 'bridge'; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedTemplate.Name) {
|
||||||
|
$scope.formValues.name = selectedTemplate.Name;
|
||||||
|
} else {
|
||||||
|
$scope.formValues.name = '';
|
||||||
|
}
|
||||||
|
|
||||||
$anchorScroll('view-top');
|
$anchorScroll('view-top');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator', 'ExtensionManager',
|
.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator',
|
||||||
function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator, ExtensionManager) {
|
function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator) {
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Driver: 'local',
|
Driver: 'local',
|
||||||
|
@ -88,11 +88,5 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtensionManager.init()
|
|
||||||
.then(function success(data) {
|
|
||||||
initView();
|
initView();
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to initialize extensions');
|
|
||||||
});
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
angular.module('extension.storidge')
|
|
||||||
.factory('StoridgeCluster', ['$http', 'StoridgeManager', function StoridgeClusterFactory($http, StoridgeManager) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var service = {};
|
|
||||||
|
|
||||||
service.queryEvents = function() {
|
|
||||||
return $http({
|
|
||||||
method: 'GET',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/events',
|
|
||||||
skipAuthorization: true,
|
|
||||||
timeout: 4500,
|
|
||||||
ignoreLoadingBar: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
service.queryVersion = function() {
|
|
||||||
return $http({
|
|
||||||
method: 'GET',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/version',
|
|
||||||
skipAuthorization: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
service.queryInfo = function() {
|
|
||||||
return $http({
|
|
||||||
method: 'GET',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/info',
|
|
||||||
skipAuthorization: true,
|
|
||||||
timeout: 4500,
|
|
||||||
ignoreLoadingBar: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
service.reboot = function() {
|
|
||||||
return $http({
|
|
||||||
method: 'POST',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/cluster/reboot',
|
|
||||||
skipAuthorization: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
service.shutdown = function() {
|
|
||||||
return $http({
|
|
||||||
method: 'POST',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/cluster/shutdown',
|
|
||||||
skipAuthorization: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return service;
|
|
||||||
}]);
|
|
|
@ -1,24 +0,0 @@
|
||||||
angular.module('extension.storidge')
|
|
||||||
.factory('StoridgeNodes', ['$http', 'StoridgeManager', function StoridgeNodesFactory($http, StoridgeManager) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var service = {};
|
|
||||||
|
|
||||||
service.query = function() {
|
|
||||||
return $http({
|
|
||||||
method: 'GET',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/nodes',
|
|
||||||
skipAuthorization: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
service.inspect = function(id) {
|
|
||||||
return $http({
|
|
||||||
method: 'GET',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/nodes/' + id,
|
|
||||||
skipAuthorization: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return service;
|
|
||||||
}]);
|
|
|
@ -1,52 +0,0 @@
|
||||||
angular.module('extension.storidge')
|
|
||||||
.factory('StoridgeProfiles', ['$http', 'StoridgeManager', function StoridgeProfilesFactory($http, StoridgeManager) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var service = {};
|
|
||||||
|
|
||||||
service.create = function(payload) {
|
|
||||||
return $http({
|
|
||||||
method: 'POST',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/profiles',
|
|
||||||
data: payload,
|
|
||||||
headers: { 'Content-type': 'application/json' },
|
|
||||||
skipAuthorization: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
service.update = function(id, payload) {
|
|
||||||
return $http({
|
|
||||||
method: 'PUT',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id,
|
|
||||||
data: payload,
|
|
||||||
headers: { 'Content-type': 'application/json' },
|
|
||||||
skipAuthorization: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
service.query = function() {
|
|
||||||
return $http({
|
|
||||||
method: 'GET',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/profiles',
|
|
||||||
skipAuthorization: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
service.inspect = function(id) {
|
|
||||||
return $http({
|
|
||||||
method: 'GET',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id,
|
|
||||||
skipAuthorization: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
service.delete = function(id) {
|
|
||||||
return $http({
|
|
||||||
method: 'DELETE',
|
|
||||||
url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id,
|
|
||||||
skipAuthorization: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return service;
|
|
||||||
}]);
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
angular.module('extension.storidge')
|
||||||
|
.factory('Storidge', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function StoridgeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/extensions/storidge/:resource/:id/:action', {
|
||||||
|
endpointId: EndpointProvider.endpointID
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rebootCluster: { method: 'POST', params: { resource: 'cluster', action: 'reboot' } },
|
||||||
|
shutdownCluster: { method: 'POST', params: { resource: 'cluster', action: 'shutdown' } },
|
||||||
|
queryEvents: { method: 'GET', params: { resource: 'events' }, timeout: 4500, ignoreLoadingBar: true, isArray: true },
|
||||||
|
getVersion: { method: 'GET', params: { resource: 'version' } },
|
||||||
|
getInfo: { method: 'GET', params: { resource: 'info' }, timeout: 4500, ignoreLoadingBar: true },
|
||||||
|
queryNodes: { method: 'GET', params: { resource: 'nodes' } },
|
||||||
|
queryProfiles: { method: 'GET', params: { resource: 'profiles' } },
|
||||||
|
getProfile: { method: 'GET', params: { resource: 'profiles' } },
|
||||||
|
createProfile: { method: 'POST', params: { resource: 'profiles' } },
|
||||||
|
updateProfile: { method: 'PUT', params: { resource: 'profiles', id: '@name' } },
|
||||||
|
deleteProfile: { method: 'DELETE', params: { resource: 'profiles' } }
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -1,22 +1,22 @@
|
||||||
angular.module('extension.storidge')
|
angular.module('extension.storidge')
|
||||||
.factory('StoridgeClusterService', ['$q', 'StoridgeCluster', function StoridgeClusterServiceFactory($q, StoridgeCluster) {
|
.factory('StoridgeClusterService', ['$q', 'Storidge', function StoridgeClusterServiceFactory($q, Storidge) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
service.reboot = function() {
|
service.reboot = function() {
|
||||||
return StoridgeCluster.reboot();
|
return Storidge.rebootCluster().$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.shutdown = function() {
|
service.shutdown = function() {
|
||||||
return StoridgeCluster.shutdown();
|
return Storidge.shutdownCluster().$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.info = function() {
|
service.info = function() {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
StoridgeCluster.queryInfo()
|
Storidge.getInfo().$promise
|
||||||
.then(function success(response) {
|
.then(function success(data) {
|
||||||
var info = new StoridgeInfoModel(response.data);
|
var info = new StoridgeInfoModel(data);
|
||||||
deferred.resolve(info);
|
deferred.resolve(info);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -29,9 +29,9 @@ angular.module('extension.storidge')
|
||||||
service.version = function() {
|
service.version = function() {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
StoridgeCluster.queryVersion()
|
Storidge.getVersion().$promise
|
||||||
.then(function success(response) {
|
.then(function success(data) {
|
||||||
var version = response.data.version;
|
var version = data.version;
|
||||||
deferred.resolve(version);
|
deferred.resolve(version);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -44,9 +44,9 @@ angular.module('extension.storidge')
|
||||||
service.events = function() {
|
service.events = function() {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
StoridgeCluster.queryEvents()
|
Storidge.queryEvents().$promise
|
||||||
.then(function success(response) {
|
.then(function success(data) {
|
||||||
var events = response.data.map(function(item) {
|
var events = data.map(function(item) {
|
||||||
return new StoridgeEventModel(item);
|
return new StoridgeEventModel(item);
|
||||||
});
|
});
|
||||||
deferred.resolve(events);
|
deferred.resolve(events);
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
angular.module('extension.storidge')
|
|
||||||
.factory('StoridgeManager', ['$q', 'LocalStorage', 'SystemService', function StoridgeManagerFactory($q, LocalStorage, SystemService) {
|
|
||||||
'use strict';
|
|
||||||
var service = {
|
|
||||||
API: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
service.init = function() {
|
|
||||||
var deferred = $q.defer();
|
|
||||||
|
|
||||||
var storedAPIURL = LocalStorage.getStoridgeAPIURL();
|
|
||||||
if (storedAPIURL) {
|
|
||||||
service.API = storedAPIURL;
|
|
||||||
deferred.resolve();
|
|
||||||
} else {
|
|
||||||
SystemService.info()
|
|
||||||
.then(function success(data) {
|
|
||||||
var endpointAddress = LocalStorage.getEndpointPublicURL();
|
|
||||||
var storidgeAPIURL = '';
|
|
||||||
if (endpointAddress) {
|
|
||||||
storidgeAPIURL = 'http://' + endpointAddress + ':8282';
|
|
||||||
} else {
|
|
||||||
var managerIP = data.Swarm.NodeAddr;
|
|
||||||
storidgeAPIURL = 'http://' + managerIP + ':8282';
|
|
||||||
}
|
|
||||||
|
|
||||||
service.API = storidgeAPIURL;
|
|
||||||
LocalStorage.storeStoridgeAPIURL(storidgeAPIURL);
|
|
||||||
deferred.resolve();
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
deferred.reject({ msg: 'Unable to retrieve Storidge API URL', err: err });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
service.reset = function() {
|
|
||||||
LocalStorage.clearStoridgeAPIURL();
|
|
||||||
};
|
|
||||||
|
|
||||||
service.StoridgeAPIURL = function() {
|
|
||||||
return service.API;
|
|
||||||
};
|
|
||||||
|
|
||||||
return service;
|
|
||||||
}]);
|
|
|
@ -1,14 +1,14 @@
|
||||||
angular.module('extension.storidge')
|
angular.module('extension.storidge')
|
||||||
.factory('StoridgeNodeService', ['$q', 'StoridgeNodes', function StoridgeNodeServiceFactory($q, StoridgeNodes) {
|
.factory('StoridgeNodeService', ['$q', 'Storidge', function StoridgeNodeServiceFactory($q, Storidge) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
service.nodes = function() {
|
service.nodes = function() {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
StoridgeNodes.query()
|
Storidge.queryNodes().$promise
|
||||||
.then(function success(response) {
|
.then(function success(data) {
|
||||||
var nodeData = response.data.nodes;
|
var nodeData = data.nodes;
|
||||||
var nodes = [];
|
var nodes = [];
|
||||||
|
|
||||||
for (var key in nodeData) {
|
for (var key in nodeData) {
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
angular.module('extension.storidge')
|
angular.module('extension.storidge')
|
||||||
.factory('StoridgeProfileService', ['$q', 'StoridgeProfiles', function StoridgeProfileServiceFactory($q, StoridgeProfiles) {
|
.factory('StoridgeProfileService', ['$q', 'Storidge', function StoridgeProfileServiceFactory($q, Storidge) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
service.create = function(model) {
|
service.create = function(model) {
|
||||||
var payload = new StoridgeCreateProfileRequest(model);
|
var payload = new StoridgeCreateProfileRequest(model);
|
||||||
return StoridgeProfiles.create(payload);
|
return Storidge.createProfile(payload).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.update = function(model) {
|
service.update = function(model) {
|
||||||
var payload = new StoridgeCreateProfileRequest(model);
|
var payload = new StoridgeCreateProfileRequest(model);
|
||||||
return StoridgeProfiles.update(model.Name, payload);
|
return Storidge.updateProfile(payload).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.delete = function(profileName) {
|
service.delete = function(profileName) {
|
||||||
return StoridgeProfiles.delete(profileName);
|
return Storidge.deleteProfile({ id: profileName }).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.profile = function(profileName) {
|
service.profile = function(profileName) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
StoridgeProfiles.inspect(profileName)
|
Storidge.getProfile({ id: profileName }).$promise
|
||||||
.then(function success(response) {
|
.then(function success(data) {
|
||||||
var profile = new StoridgeProfileModel(profileName, response.data);
|
var profile = new StoridgeProfileModel(profileName, data);
|
||||||
deferred.resolve(profile);
|
deferred.resolve(profile);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -35,9 +35,9 @@ angular.module('extension.storidge')
|
||||||
service.profiles = function() {
|
service.profiles = function() {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
StoridgeProfiles.query()
|
Storidge.queryProfiles().$promise
|
||||||
.then(function success(response) {
|
.then(function success(data) {
|
||||||
var profiles = response.data.profiles.map(function (item) {
|
var profiles = data.profiles.map(function (item) {
|
||||||
return new StoridgeProfileListModel(item);
|
return new StoridgeProfileListModel(item);
|
||||||
});
|
});
|
||||||
deferred.resolve(profiles);
|
deferred.resolve(profiles);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('extension.storidge')
|
angular.module('extension.storidge')
|
||||||
.controller('StoridgeClusterController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeClusterService', 'StoridgeNodeService', 'StoridgeManager', 'ModalService',
|
.controller('StoridgeClusterController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeClusterService', 'StoridgeNodeService', 'ModalService',
|
||||||
function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNodeService, StoridgeManager, ModalService) {
|
function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNodeService, ModalService) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
shutdownInProgress: false,
|
shutdownInProgress: false,
|
||||||
|
@ -44,30 +44,20 @@ function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNod
|
||||||
function shutdownCluster() {
|
function shutdownCluster() {
|
||||||
$scope.state.shutdownInProgress = true;
|
$scope.state.shutdownInProgress = true;
|
||||||
StoridgeClusterService.shutdown()
|
StoridgeClusterService.shutdown()
|
||||||
.then(function success(data) {
|
|
||||||
Notifications.success('Cluster successfully shutdown');
|
|
||||||
$state.go('docker.dashboard');
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to shutdown cluster');
|
|
||||||
})
|
|
||||||
.finally(function final() {
|
.finally(function final() {
|
||||||
$scope.state.shutdownInProgress = false;
|
$scope.state.shutdownInProgress = false;
|
||||||
|
Notifications.success('Cluster successfully shutdown');
|
||||||
|
$state.go('docker.dashboard');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebootCluster() {
|
function rebootCluster() {
|
||||||
$scope.state.rebootInProgress = true;
|
$scope.state.rebootInProgress = true;
|
||||||
StoridgeClusterService.reboot()
|
StoridgeClusterService.reboot()
|
||||||
.then(function success(data) {
|
|
||||||
Notifications.success('Cluster successfully rebooted');
|
|
||||||
$state.reload();
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to reboot cluster');
|
|
||||||
})
|
|
||||||
.finally(function final() {
|
.finally(function final() {
|
||||||
$scope.state.rebootInProgress = false;
|
$scope.state.rebootInProgress = false;
|
||||||
|
Notifications.success('Cluster successfully rebooted');
|
||||||
|
$state.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,11 +77,5 @@ function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNod
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
StoridgeManager.init()
|
|
||||||
.then(function success() {
|
|
||||||
initView();
|
initView();
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to communicate with Storidge API');
|
|
||||||
});
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('extension.storidge')
|
angular.module('extension.storidge')
|
||||||
.controller('StoridgeMonitorController', ['$q', '$scope', '$interval', '$document', 'Notifications', 'StoridgeClusterService', 'StoridgeChartService', 'StoridgeManager', 'ModalService',
|
.controller('StoridgeMonitorController', ['$q', '$scope', '$interval', '$document', 'Notifications', 'StoridgeClusterService', 'StoridgeChartService', 'ModalService',
|
||||||
function ($q, $scope, $interval, $document, Notifications, StoridgeClusterService, StoridgeChartService, StoridgeManager, ModalService) {
|
function ($q, $scope, $interval, $document, Notifications, StoridgeClusterService, StoridgeChartService, ModalService) {
|
||||||
|
|
||||||
$scope.$on('$destroy', function() {
|
$scope.$on('$destroy', function() {
|
||||||
stopRepeater();
|
stopRepeater();
|
||||||
|
@ -98,11 +98,5 @@ function ($q, $scope, $interval, $document, Notifications, StoridgeClusterServic
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
StoridgeManager.init()
|
|
||||||
.then(function success() {
|
|
||||||
initView();
|
initView();
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to communicate with Storidge API');
|
|
||||||
});
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('extension.storidge')
|
angular.module('extension.storidge')
|
||||||
.controller('StoridgeCreateProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'StoridgeManager',
|
.controller('StoridgeCreateProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService',
|
||||||
function ($scope, $state, $transition$, Notifications, StoridgeProfileService, StoridgeManager) {
|
function ($scope, $state, $transition$, Notifications, StoridgeProfileService) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
NoLimit: true,
|
NoLimit: true,
|
||||||
|
@ -62,11 +62,5 @@ function ($scope, $state, $transition$, Notifications, StoridgeProfileService, S
|
||||||
$scope.model = profile;
|
$scope.model = profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
StoridgeManager.init()
|
|
||||||
.then(function success() {
|
|
||||||
initView();
|
initView();
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to communicate with Storidge API');
|
|
||||||
});
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('extension.storidge')
|
angular.module('extension.storidge')
|
||||||
.controller('StoridgeProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'StoridgeManager', 'ModalService',
|
.controller('StoridgeProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'ModalService',
|
||||||
function ($scope, $state, $transition$, Notifications, StoridgeProfileService, StoridgeManager, ModalService) {
|
function ($scope, $state, $transition$, Notifications, StoridgeProfileService, ModalService) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
NoLimit: false,
|
NoLimit: false,
|
||||||
|
@ -88,11 +88,6 @@ function ($scope, $state, $transition$, Notifications, StoridgeProfileService, S
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
StoridgeManager.init()
|
|
||||||
.then(function success() {
|
|
||||||
initView();
|
initView();
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to communicate with Storidge API');
|
|
||||||
});
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('extension.storidge')
|
angular.module('extension.storidge')
|
||||||
.controller('StoridgeProfilesController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeProfileService', 'StoridgeManager',
|
.controller('StoridgeProfilesController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeProfileService',
|
||||||
function ($q, $scope, $state, Notifications, StoridgeProfileService, StoridgeManager) {
|
function ($q, $scope, $state, Notifications, StoridgeProfileService) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false
|
actionInProgress: false
|
||||||
|
@ -60,11 +60,5 @@ function ($q, $scope, $state, Notifications, StoridgeProfileService, StoridgeMan
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
StoridgeManager.init()
|
|
||||||
.then(function success() {
|
|
||||||
initView();
|
initView();
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to communicate with Storidge API');
|
|
||||||
});
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
angular.module('portainer.app').component('codeEditor', {
|
||||||
|
templateUrl: 'app/portainer/components/code-editor/codeEditor.html',
|
||||||
|
controller: 'CodeEditorController',
|
||||||
|
bindings: {
|
||||||
|
identifier: '@',
|
||||||
|
placeholder: '@',
|
||||||
|
yml: '<',
|
||||||
|
readOnly: '<',
|
||||||
|
onChange: '<',
|
||||||
|
value: '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
<textarea id="{{ $ctrl.identifier }}" class="form-control" placeholder="{{ $ctrl.placeholder }}"></textarea>
|
|
@ -0,0 +1,18 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.controller('CodeEditorController', ['$document', 'CodeMirrorService',
|
||||||
|
function ($document, CodeMirrorService) {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
this.$onInit = function() {
|
||||||
|
$document.ready(function() {
|
||||||
|
var editorElement = $document[0].getElementById(ctrl.identifier);
|
||||||
|
ctrl.editor = CodeMirrorService.applyCodeMirrorOnElement(editorElement, ctrl.yml, ctrl.readOnly);
|
||||||
|
if (ctrl.onChange) {
|
||||||
|
ctrl.editor.on('change', ctrl.onChange);
|
||||||
|
}
|
||||||
|
if (ctrl.value) {
|
||||||
|
ctrl.editor.setValue(ctrl.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -0,0 +1,10 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('Extensions', ['$resource', 'EndpointProvider', 'API_ENDPOINT_ENDPOINTS', function Extensions($resource, EndpointProvider, API_ENDPOINT_ENDPOINTS) {
|
||||||
|
'use strict';
|
||||||
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/extensions', {
|
||||||
|
endpointId: EndpointProvider.endpointID
|
||||||
|
},
|
||||||
|
{
|
||||||
|
register: { method: 'POST', params: { endpointId: '@endpointId' } }
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -66,6 +66,7 @@ angular.module('portainer.app')
|
||||||
TLSSkipVerify: TLSSkipVerify,
|
TLSSkipVerify: TLSSkipVerify,
|
||||||
TLSSkipClientVerify: TLSSkipClientVerify
|
TLSSkipClientVerify: TLSSkipClientVerify
|
||||||
};
|
};
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
Endpoints.create({}, endpoint).$promise
|
Endpoints.create({}, endpoint).$promise
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
|
@ -85,6 +86,7 @@ angular.module('portainer.app')
|
||||||
deferred.notify({upload: false});
|
deferred.notify({upload: false});
|
||||||
deferred.reject({msg: 'Unable to upload TLS certs', err: err});
|
deferred.reject({msg: 'Unable to upload TLS certs', err: err});
|
||||||
});
|
});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue