mirror of https://github.com/portainer/portainer
Merge remote-tracking branch 'origin/develop' into webpack
commit
71447e2cc0
|
@ -6,7 +6,7 @@
|
|||
[](https://hub.docker.com/r/portainer/portainer/)
|
||||
[](http://microbadger.com/images/portainer/portainer "Image size")
|
||||
[](http://portainer.readthedocs.io/en/stable/?badge=stable)
|
||||
[](https://semaphoreci.com/portainer/portainer)
|
||||
[](https://semaphoreci.com/portainer/portainer-ci)
|
||||
[](https://codeclimate.com/github/portainer/portainer)
|
||||
[](https://portainer.io/slack/)
|
||||
[](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
|
|
@ -7,13 +7,13 @@ import (
|
|||
|
||||
// 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) {
|
||||
func TarFileInBuffer(fileContent []byte, fileName string, mode int64) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
tarWriter := tar.NewWriter(&buffer)
|
||||
|
||||
header := &tar.Header{
|
||||
Name: fileName,
|
||||
Mode: 0600,
|
||||
Mode: mode,
|
||||
Size: int64(len(fileContent)),
|
||||
}
|
||||
|
||||
|
|
|
@ -383,6 +383,10 @@ func initEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointS
|
|||
return createUnsecuredEndpoint(*flags.EndpointURL, endpointService, snapshotter)
|
||||
}
|
||||
|
||||
func initJobService(dockerClientFactory *docker.ClientFactory) portainer.JobService {
|
||||
return docker.NewJobService(dockerClientFactory)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flags := initCLI()
|
||||
|
||||
|
@ -408,6 +412,8 @@ func main() {
|
|||
|
||||
clientFactory := initClientFactory(digitalSignatureService)
|
||||
|
||||
jobService := initJobService(clientFactory)
|
||||
|
||||
snapshotter := initSnapshotter(clientFactory)
|
||||
|
||||
jobScheduler, err := initJobScheduler(store.EndpointService, snapshotter, flags)
|
||||
|
@ -520,6 +526,7 @@ func main() {
|
|||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
DockerClientFactory: clientFactory,
|
||||
JobService: jobService,
|
||||
}
|
||||
|
||||
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
||||
|
|
|
@ -3,7 +3,6 @@ package docker
|
|||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer"
|
||||
|
@ -27,12 +26,13 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService) *Clien
|
|||
}
|
||||
|
||||
// CreateClient is a generic function to create a Docker client based on
|
||||
// a specific endpoint configuration
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
// a specific endpoint configuration. The nodeName parameter can be used
|
||||
// with an agent enabled endpoint to target a specific node in an agent cluster.
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
return nil, unsupportedEnvironmentType
|
||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
return createAgentClient(endpoint, factory.signatureService)
|
||||
return createAgentClient(endpoint, factory.signatureService, nodeName)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||
|
@ -61,7 +61,7 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
|||
)
|
||||
}
|
||||
|
||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService) (*client.Client, error) {
|
||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -77,6 +77,10 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
|
|||
portainer.PortainerAgentSignatureHeader: signature,
|
||||
}
|
||||
|
||||
if nodeName != "" {
|
||||
headers[portainer.PortainerAgentTargetHeader] = nodeName
|
||||
}
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
|
@ -97,7 +101,6 @@ func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) {
|
|||
}
|
||||
|
||||
return &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Transport: transport,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/strslice"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/archive"
|
||||
)
|
||||
|
||||
// JobService represnts a service that handles jobs on the host
|
||||
type JobService struct {
|
||||
DockerClientFactory *ClientFactory
|
||||
}
|
||||
|
||||
// NewJobService returns a pointer to a new job service
|
||||
func NewJobService(dockerClientFactory *ClientFactory) *JobService {
|
||||
return &JobService{
|
||||
DockerClientFactory: dockerClientFactory,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute will execute a script on the endpoint host with the supplied image as a container
|
||||
func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image string, script []byte) error {
|
||||
buffer, err := archive.TarFileInBuffer(script, "script.sh", 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli, err := service.DockerClientFactory.CreateClient(endpoint, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
err = pullImage(cli, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containerConfig := &container.Config{
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
WorkingDir: "/tmp",
|
||||
Image: image,
|
||||
Labels: map[string]string{
|
||||
"io.portainer.job.endpoint": strconv.Itoa(int(endpoint.ID)),
|
||||
},
|
||||
Cmd: strslice.StrSlice([]string{"sh", "/tmp/script.sh"}),
|
||||
}
|
||||
|
||||
hostConfig := &container.HostConfig{
|
||||
Binds: []string{"/:/host", "/etc:/etc:ro", "/usr:/usr:ro", "/run:/run:ro", "/sbin:/sbin:ro", "/var:/var:ro"},
|
||||
NetworkMode: "host",
|
||||
Privileged: true,
|
||||
}
|
||||
|
||||
networkConfig := &network.NetworkingConfig{}
|
||||
|
||||
body, err := cli.ContainerCreate(context.Background(), containerConfig, hostConfig, networkConfig, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copyOptions := types.CopyToContainerOptions{}
|
||||
err = cli.CopyToContainer(context.Background(), body.ID, "/tmp", bytes.NewReader(buffer), copyOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startOptions := types.ContainerStartOptions{}
|
||||
err = cli.ContainerStart(context.Background(), body.ID, startOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pullImage(cli *client.Client, image string) error {
|
||||
imageReadCloser, err := cli.ImagePull(context.Background(), image, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer imageReadCloser.Close()
|
||||
|
||||
_, err = io.Copy(ioutil.Discard, imageReadCloser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -18,7 +18,7 @@ func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
|
|||
|
||||
// CreateSnapshot creates a snapshot of a specific endpoint
|
||||
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.Snapshot, error) {
|
||||
cli, err := snapshotter.clientFactory.CreateClient(endpoint)
|
||||
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type endpointJobFromFilePayload struct {
|
||||
Image string
|
||||
File []byte
|
||||
}
|
||||
|
||||
type endpointJobFromFileContentPayload struct {
|
||||
Image string
|
||||
FileContent string
|
||||
}
|
||||
|
||||
func (payload *endpointJobFromFilePayload) Validate(r *http.Request) error {
|
||||
file, _, err := request.RetrieveMultiPartFormFile(r, "File")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Script file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.File = file
|
||||
|
||||
image, err := request.RetrieveMultiPartFormValue(r, "Image", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid image name")
|
||||
}
|
||||
payload.Image = image
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *endpointJobFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.FileContent) {
|
||||
return portainer.Error("Invalid script file content")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.Image) {
|
||||
return portainer.Error("Invalid image name")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/endpoints/:id/job?method&nodeName
|
||||
func (handler *Handler) endpointJob(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
|
||||
}
|
||||
|
||||
nodeName, _ := request.RetrieveQueryParameter(r, "nodeName", true)
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "file":
|
||||
return handler.executeJobFromFile(w, r, endpoint, nodeName)
|
||||
case "string":
|
||||
return handler.executeJobFromFileContent(w, r, endpoint, nodeName)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string or file", errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
|
||||
func (handler *Handler) executeJobFromFile(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, nodeName string) *httperror.HandlerError {
|
||||
payload := &endpointJobFromFilePayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
err = handler.JobService.Execute(endpoint, nodeName, payload.Image, payload.File)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) executeJobFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, nodeName string) *httperror.HandlerError {
|
||||
var payload endpointJobFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
err = handler.JobService.Execute(endpoint, nodeName, payload.Image, []byte(payload.FileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -31,6 +31,7 @@ type Handler struct {
|
|||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
Snapshotter portainer.Snapshotter
|
||||
JobService portainer.JobService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint operations.
|
||||
|
@ -59,6 +60,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
|
|||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/extensions/{extensionType}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||
|
||||
h.Handle("/endpoints/{id}/job",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
|
||||
func (handler *Handler) executeServiceWebhook(w http.ResponseWriter, endpoint *portainer.Endpoint, resourceID string) *httperror.HandlerError {
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint)
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Error creating docker client", err}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ func buildOperation(request *http.Request) error {
|
|||
dockerfileContent = []byte(req.Content)
|
||||
}
|
||||
|
||||
buffer, err := archive.TarFileInBuffer(dockerfileContent, "Dockerfile")
|
||||
buffer, err := archive.TarFileInBuffer(dockerfileContent, "Dockerfile", 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ type Server struct {
|
|||
SSLCert string
|
||||
SSLKey string
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
JobService portainer.JobService
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
|
@ -109,6 +110,7 @@ func (server *Server) Start() error {
|
|||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = proxyManager
|
||||
endpointHandler.Snapshotter = server.Snapshotter
|
||||
endpointHandler.JobService = server.JobService
|
||||
|
||||
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
||||
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
|
||||
|
|
|
@ -635,6 +635,11 @@ type (
|
|||
Up(stack *Stack, endpoint *Endpoint) error
|
||||
Down(stack *Stack, endpoint *Endpoint) error
|
||||
}
|
||||
|
||||
// JobService represents a service to manage job execution on hosts
|
||||
JobService interface {
|
||||
Execute(endpoint *Endpoint, nodeName, image string, script []byte) error
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
222
api/swagger.yaml
222
api/swagger.yaml
|
@ -153,6 +153,8 @@ paths:
|
|||
operationId: "DockerHubInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -175,6 +177,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -211,6 +215,8 @@ paths:
|
|||
operationId: "EndpointList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -233,6 +239,8 @@ paths:
|
|||
- "multipart/form-data"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "Name"
|
||||
in: "formData"
|
||||
|
@ -265,7 +273,7 @@ paths:
|
|||
- name: "TLSSkipVerify"
|
||||
in: "formData"
|
||||
type: "string"
|
||||
description: "Skip server verification when using TLS" (example: false)
|
||||
description: "Skip server verification when using TLS (example: false)"
|
||||
- name: "TLSCACertFile"
|
||||
in: "formData"
|
||||
type: "file"
|
||||
|
@ -324,6 +332,8 @@ paths:
|
|||
operationId: "EndpointInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -365,6 +375,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -413,6 +425,8 @@ paths:
|
|||
Remove an endpoint.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -460,6 +474,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -495,6 +511,78 @@ paths:
|
|||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/endpoints/{id}/job:
|
||||
post:
|
||||
tags:
|
||||
- "endpoints"
|
||||
summary: "Execute a job on the endpoint host"
|
||||
description: |
|
||||
Execute a job (script) on the underlying host of the endpoint.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointJob"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Endpoint identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
- name: "method"
|
||||
in: "query"
|
||||
description: "Job execution method. Possible values: file or string."
|
||||
required: true
|
||||
type: "string"
|
||||
- name: "nodeName"
|
||||
in: "query"
|
||||
description: "Optional. Hostname of a node when targeting a Portainer agent cluster."
|
||||
required: true
|
||||
type: "string"
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Job details. Required when method equals string."
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/EndpointJobRequest"
|
||||
- name: "Image"
|
||||
in: "formData"
|
||||
type: "string"
|
||||
description: "Container image which will be used to execute the job. Required when method equals file."
|
||||
- name: "file"
|
||||
in: "formData"
|
||||
type: "file"
|
||||
description: "Job script file. Required when method equals file."
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/Endpoint"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
404:
|
||||
description: "Endpoint not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Endpoint not found"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/endpoint_groups:
|
||||
get:
|
||||
tags:
|
||||
|
@ -508,6 +596,8 @@ paths:
|
|||
operationId: "EndpointGroupList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -530,6 +620,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -564,6 +656,8 @@ paths:
|
|||
operationId: "EndpointGroupInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -605,6 +699,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -655,6 +751,8 @@ paths:
|
|||
Remove an endpoint group.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointGroupDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -702,6 +800,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -750,6 +850,8 @@ paths:
|
|||
operationId: "RegistryList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -772,6 +874,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -813,6 +917,8 @@ paths:
|
|||
operationId: "RegistryInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -854,6 +960,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -904,6 +1012,8 @@ paths:
|
|||
Remove a registry.
|
||||
**Access policy**: administrator
|
||||
operationId: "RegistryDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -944,6 +1054,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -992,6 +1104,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -1042,6 +1156,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1092,6 +1208,8 @@ paths:
|
|||
Remove a resource control.
|
||||
**Access policy**: restricted
|
||||
operationId: "ResourceControlDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1137,6 +1255,8 @@ paths:
|
|||
operationId: "SettingsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -1159,6 +1279,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -1193,6 +1315,8 @@ paths:
|
|||
operationId: "PublicSettingsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -1216,6 +1340,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -1248,6 +1374,8 @@ paths:
|
|||
operationId: "StatusInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -1271,6 +1399,8 @@ paths:
|
|||
operationId: "StackList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "filters"
|
||||
in: "query"
|
||||
|
@ -1303,6 +1433,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "type"
|
||||
in: "query"
|
||||
|
@ -1382,6 +1514,8 @@ paths:
|
|||
operationId: "StackInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1427,6 +1561,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1479,6 +1615,8 @@ paths:
|
|||
Remove a stack.
|
||||
**Access policy**: restricted
|
||||
operationId: "StackDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1529,6 +1667,8 @@ paths:
|
|||
operationId: "StackFileInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1574,6 +1714,8 @@ paths:
|
|||
operationId: "StackMigrate"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1628,6 +1770,8 @@ paths:
|
|||
operationId: "UserList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -1651,6 +1795,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -1699,6 +1845,8 @@ paths:
|
|||
operationId: "UserInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1740,6 +1888,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1790,6 +1940,8 @@ paths:
|
|||
Remove a user.
|
||||
**Access policy**: administrator
|
||||
operationId: "UserDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1828,6 +1980,8 @@ paths:
|
|||
operationId: "UserMembershipsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1871,6 +2025,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -1918,6 +2074,8 @@ paths:
|
|||
operationId: "UserAdminCheck"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
204:
|
||||
|
@ -1947,6 +2105,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -1991,6 +2151,8 @@ paths:
|
|||
- multipart/form-data
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "path"
|
||||
name: "certificate"
|
||||
|
@ -2032,6 +2194,8 @@ paths:
|
|||
operationId: "TagList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -2054,6 +2218,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -2093,6 +2259,8 @@ paths:
|
|||
Remove a tag.
|
||||
**Access policy**: administrator
|
||||
operationId: "TagDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -2125,6 +2293,8 @@ paths:
|
|||
operationId: "TeamList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -2147,6 +2317,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -2195,6 +2367,8 @@ paths:
|
|||
operationId: "TeamInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -2243,6 +2417,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -2284,6 +2460,8 @@ paths:
|
|||
Remove a team.
|
||||
**Access policy**: administrator
|
||||
operationId: "TeamDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -2323,6 +2501,8 @@ paths:
|
|||
operationId: "TeamMembershipsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -2364,6 +2544,8 @@ paths:
|
|||
operationId: "TeamMembershipList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
|
@ -2393,6 +2575,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -2443,6 +2627,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -2493,6 +2679,8 @@ paths:
|
|||
Remove a team membership. Access is only available to administrators leaders of the associated team.
|
||||
**Access policy**: restricted
|
||||
operationId: "TeamMembershipDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -2539,6 +2727,8 @@ paths:
|
|||
operationId: "TemplateList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
responses:
|
||||
200:
|
||||
|
@ -2561,6 +2751,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
|
@ -2602,6 +2794,8 @@ paths:
|
|||
operationId: "TemplateInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -2650,6 +2844,8 @@ paths:
|
|||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -2698,6 +2894,8 @@ paths:
|
|||
Remove a template.
|
||||
**Access policy**: administrator
|
||||
operationId: "TemplateDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
|
@ -3783,9 +3981,9 @@ definitions:
|
|||
TemplateCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "type"
|
||||
- "title"
|
||||
- "description"
|
||||
- "type"
|
||||
- "title"
|
||||
- "description"
|
||||
properties:
|
||||
type:
|
||||
type: "integer"
|
||||
|
@ -4137,7 +4335,7 @@ definitions:
|
|||
TemplateRepository:
|
||||
type: "object"
|
||||
required:
|
||||
- "URL"
|
||||
- "URL"
|
||||
properties:
|
||||
URL:
|
||||
type: "string"
|
||||
|
@ -4164,6 +4362,20 @@ definitions:
|
|||
type: "string"
|
||||
example: "new-stack"
|
||||
description: "If provided will rename the migrated stack"
|
||||
EndpointJobRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "Image"
|
||||
- "FileContent"
|
||||
properties:
|
||||
Image:
|
||||
type: "string"
|
||||
example: "ubuntu:latest"
|
||||
description: "Container image which will be used to execute the job"
|
||||
FileContent:
|
||||
type: "string"
|
||||
example: "ls -lah /host/tmp"
|
||||
description: "Content of the job script"
|
||||
StackCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"packageName": "portainer",
|
||||
"packageVersion": "1.20-dev",
|
||||
"projectName": "portainer"
|
||||
}
|
|
@ -3,6 +3,7 @@ angular.module('portainer.agent').component('volumeBrowser', {
|
|||
controller: 'VolumeBrowserController',
|
||||
bindings: {
|
||||
volumeId: '<',
|
||||
nodeName: '<'
|
||||
nodeName: '<',
|
||||
isUploadEnabled: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -8,4 +8,7 @@
|
|||
rename="$ctrl.rename(name, newName)"
|
||||
download="$ctrl.download(name)"
|
||||
delete="$ctrl.delete(name)"
|
||||
|
||||
is-upload-allowed="$ctrl.isUploadEnabled"
|
||||
on-file-selected-for-upload="$ctrl.onFileSelectedForUpload"
|
||||
></files-datatable>
|
||||
|
|
|
@ -87,6 +87,16 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService
|
|||
});
|
||||
}
|
||||
|
||||
this.onFileSelectedForUpload = function onFileSelectedForUpload(file) {
|
||||
VolumeBrowserService.upload(ctrl.state.path, file, ctrl.volumeId)
|
||||
.then(function onFileUpload() {
|
||||
onFileUploaded();
|
||||
})
|
||||
.catch(function onFileUpload(err) {
|
||||
Notifications.error('Failure', err, 'Unable to upload file');
|
||||
});
|
||||
};
|
||||
|
||||
function parentPath(path) {
|
||||
if (path.lastIndexOf('/') === 0) {
|
||||
return '/';
|
||||
|
@ -115,4 +125,14 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService
|
|||
});
|
||||
};
|
||||
|
||||
function onFileUploaded() {
|
||||
refreshList();
|
||||
}
|
||||
|
||||
function refreshList() {
|
||||
browse(ctrl.state.path);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}]);
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
angular.module('portainer.agent')
|
||||
.factory('Agent', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
.factory('Agent', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager',
|
||||
function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/agents', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/agents', {
|
||||
endpointId: EndpointProvider.endpointID,
|
||||
version: StateManager.getAgentApiVersion
|
||||
},
|
||||
{
|
||||
query: { method: 'GET', isArray: true }
|
||||
|
|
|
@ -2,10 +2,12 @@ import angular from 'angular';
|
|||
import { browseGetResponse } from './response/browse';
|
||||
|
||||
angular.module('portainer.agent')
|
||||
.factory('Browse', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
.factory('Browse', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager',
|
||||
function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:action', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/browse/:action', {
|
||||
endpointId: EndpointProvider.endpointID,
|
||||
version: StateManager.getAgentApiVersion
|
||||
},
|
||||
{
|
||||
ls: {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
angular.module('portainer.agent').factory('Host', [
|
||||
'$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
|
||||
function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
'$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager',
|
||||
function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) {
|
||||
'use strict';
|
||||
return $resource(
|
||||
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/host/:action',
|
||||
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/host/:action',
|
||||
{
|
||||
endpointId: EndpointProvider.endpointID
|
||||
endpointId: EndpointProvider.endpointID,
|
||||
version: StateManager.getAgentApiVersion
|
||||
},
|
||||
{
|
||||
info: { method: 'GET', params: { action: 'info' } }
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
angular.module('portainer.agent').factory('AgentPing', [
|
||||
'$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', '$q',
|
||||
function AgentPingFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q) {
|
||||
'use strict';
|
||||
return $resource(
|
||||
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/ping',
|
||||
{
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
||||
ping: {
|
||||
method: 'GET',
|
||||
interceptor: {
|
||||
response: function versionInterceptor(response) {
|
||||
var instance = response.resource;
|
||||
var version =
|
||||
response.headers('Portainer-Agent-Api-Version') || 1;
|
||||
instance.version = Number(version);
|
||||
return instance;
|
||||
},
|
||||
responseError: function versionResponseError(error) {
|
||||
// 404 - agent is up - set version to 1
|
||||
if (error.status === 404) {
|
||||
return { version: 1 };
|
||||
}
|
||||
return $q.reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
]);
|
|
@ -0,0 +1,10 @@
|
|||
angular.module('portainer.agent')
|
||||
.factory('AgentVersion1', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/agents', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
||||
query: { method: 'GET', isArray: true }
|
||||
});
|
||||
}]);
|
|
@ -0,0 +1,25 @@
|
|||
import angular from 'angular';
|
||||
import { browseGetResponse } from '../response/browse';
|
||||
|
||||
angular.module('portainer.agent')
|
||||
.factory('BrowseVersion1', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:volumeID/:action', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
||||
ls: {
|
||||
method: 'GET', isArray: true, params: { action: 'ls' }
|
||||
},
|
||||
get: {
|
||||
method: 'GET', params: { action: 'get' },
|
||||
transformResponse: browseGetResponse
|
||||
},
|
||||
delete: {
|
||||
method: 'DELETE', params: { action: 'delete' }
|
||||
},
|
||||
rename: {
|
||||
method: 'PUT', params: { action: 'rename' }
|
||||
}
|
||||
});
|
||||
}]);
|
|
@ -2,14 +2,19 @@ import angular from 'angular';
|
|||
import { AgentViewModel } from '../models/agent';
|
||||
|
||||
angular.module('portainer.agent').factory('AgentService', [
|
||||
'$q', 'Agent','HttpRequestHelper', 'Host',
|
||||
function AgentServiceFactory($q, Agent, HttpRequestHelper, Host) {
|
||||
'$q', 'Agent', 'AgentVersion1', 'HttpRequestHelper', 'Host', 'StateManager',
|
||||
function AgentServiceFactory($q, Agent, AgentVersion1, HttpRequestHelper, Host, StateManager) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.agents = agents;
|
||||
service.hostInfo = hostInfo;
|
||||
|
||||
function getAgentApiVersion() {
|
||||
var state = StateManager.getState();
|
||||
return state.endpoint.agentApiVersion;
|
||||
}
|
||||
|
||||
function hostInfo(nodeName) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
return Host.info().$promise;
|
||||
|
@ -18,7 +23,10 @@ angular.module('portainer.agent').factory('AgentService', [
|
|||
function agents() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Agent.query({})
|
||||
var agentVersion = getAgentApiVersion();
|
||||
var service = agentVersion > 1 ? Agent : AgentVersion1;
|
||||
|
||||
service.query({ version: agentVersion })
|
||||
.$promise.then(function success(data) {
|
||||
var agents = data.map(function(item) {
|
||||
return new AgentViewModel(item);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.agent').factory('HostBrowserService', [
|
||||
'Browse', 'Upload', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', '$q',
|
||||
function HostBrowserServiceFactory(Browse, Upload, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q) {
|
||||
'Browse', 'Upload', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', '$q', 'StateManager',
|
||||
function HostBrowserServiceFactory(Browse, Upload, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q, StateManager) {
|
||||
var service = {};
|
||||
|
||||
service.ls = ls;
|
||||
|
@ -31,9 +31,17 @@ angular.module('portainer.agent').factory('HostBrowserService', [
|
|||
|
||||
function upload(path, file, onProgress) {
|
||||
var deferred = $q.defer();
|
||||
var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/browse/put';
|
||||
var agentVersion = StateManager.getAgentApiVersion();
|
||||
var url =
|
||||
API_ENDPOINT_ENDPOINTS +
|
||||
'/' +
|
||||
EndpointProvider.endpointID() +
|
||||
'/docker' +
|
||||
(agentVersion > 1 ? '/v' + agentVersion : '') +
|
||||
'/browse/put';
|
||||
|
||||
Upload.upload({
|
||||
url: url,
|
||||
url: url,
|
||||
data: { file: file, Path: path }
|
||||
}).then(deferred.resolve, deferred.reject, onProgress);
|
||||
return deferred.promise;
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
angular.module('portainer.agent').service('AgentPingService', [
|
||||
'AgentPing',
|
||||
function AgentPingService(AgentPing) {
|
||||
var service = {};
|
||||
|
||||
service.ping = ping;
|
||||
|
||||
function ping() {
|
||||
return AgentPing.ping().$promise;
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
]);
|
|
@ -1,27 +1,60 @@
|
|||
angular.module('portainer.agent').factory('VolumeBrowserService', [
|
||||
'$q', 'Browse',
|
||||
function VolumeBrowserServiceFactory($q, Browse) {
|
||||
'StateManager', 'Browse', 'BrowseVersion1', '$q', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'Upload',
|
||||
function VolumeBrowserServiceFactory(StateManager, Browse, BrowseVersion1, $q, API_ENDPOINT_ENDPOINTS, EndpointProvider, Upload) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
function getAgentApiVersion() {
|
||||
var state = StateManager.getState();
|
||||
return state.endpoint.agentApiVersion;
|
||||
}
|
||||
|
||||
function getBrowseService() {
|
||||
var agentVersion = getAgentApiVersion();
|
||||
return agentVersion > 1 ? Browse : BrowseVersion1;
|
||||
}
|
||||
|
||||
service.ls = function(volumeId, path) {
|
||||
return Browse.ls({ volumeID: volumeId, path: path }).$promise;
|
||||
return getBrowseService().ls({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise;
|
||||
};
|
||||
|
||||
service.get = function(volumeId, path) {
|
||||
return Browse.get({ volumeID: volumeId, path: path }).$promise;
|
||||
return getBrowseService().get({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise;
|
||||
};
|
||||
|
||||
service.delete = function(volumeId, path) {
|
||||
return Browse.delete({ volumeID: volumeId, path: path }).$promise;
|
||||
return getBrowseService().delete({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise;
|
||||
};
|
||||
|
||||
service.rename = function(volumeId, path, newPath) {
|
||||
var payload = {
|
||||
CurrentFilePath: path,
|
||||
CurrentFilePath: path,
|
||||
NewFilePath: newPath
|
||||
};
|
||||
return Browse.rename({ volumeID: volumeId }, payload).$promise;
|
||||
return getBrowseService().rename({ volumeID: volumeId, version: getAgentApiVersion() }, payload).$promise;
|
||||
};
|
||||
|
||||
service.upload = function upload(path, file, volumeId, onProgress) {
|
||||
var deferred = $q.defer();
|
||||
var agentVersion = StateManager.getAgentApiVersion();
|
||||
if (agentVersion <2) {
|
||||
deferred.reject('upload is not supported on this agent version');
|
||||
return;
|
||||
}
|
||||
var url =
|
||||
API_ENDPOINT_ENDPOINTS +
|
||||
'/' +
|
||||
EndpointProvider.endpointID() +
|
||||
'/docker' +
|
||||
'/v' + agentVersion +
|
||||
'/browse/put?volumeID=' +
|
||||
volumeId;
|
||||
|
||||
Upload.upload({
|
||||
url: url,
|
||||
data: { file: file, Path: path }
|
||||
}).then(deferred.resolve, deferred.reject, onProgress);
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
|
|
|
@ -37,9 +37,13 @@ function ($rootScope, $state, Authentication, authManager, StateManager, Endpoin
|
|||
|
||||
function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
||||
authManager.checkAuthOnRefresh();
|
||||
authManager.redirectWhenUnauthenticated();
|
||||
Authentication.init();
|
||||
$rootScope.$on('tokenHasExpired', function() {
|
||||
|
||||
// The unauthenticated event is broadcasted by the jwtInterceptor when
|
||||
// hitting a 401. We're using this instead of the usual combination of
|
||||
// authManager.redirectWhenUnauthenticated() + unauthenticatedRedirector
|
||||
// to have more controls on which URL should trigger the unauthenticated state.
|
||||
$rootScope.$on('unauthenticated', function () {
|
||||
$state.go('portainer.auth', {error: 'Your session has expired'});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,9 +19,6 @@ angular.module('portainer')
|
|||
jwtOptionsProvider.config({
|
||||
tokenGetter: ['LocalStorage', function(LocalStorage) {
|
||||
return LocalStorage.getJWT();
|
||||
}],
|
||||
unauthenticatedRedirector: ['$state', function($state) {
|
||||
$state.go('portainer.auth', {error: 'Your session has expired'});
|
||||
}]
|
||||
});
|
||||
$httpProvider.interceptors.push('jwtInterceptor');
|
||||
|
|
|
@ -151,6 +151,16 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
}
|
||||
};
|
||||
|
||||
var hostJob = {
|
||||
name: 'docker.host.job',
|
||||
url: '/job',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'hostJobView'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var events = {
|
||||
name: 'docker.events',
|
||||
url: '/events',
|
||||
|
@ -265,6 +275,16 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
}
|
||||
};
|
||||
|
||||
var nodeJob = {
|
||||
name: 'docker.nodes.node.job',
|
||||
url: '/job',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'nodeJobView'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var secrets = {
|
||||
name: 'docker.secrets',
|
||||
url: '/secrets',
|
||||
|
@ -436,7 +456,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
$stateRegistryProvider.register(configs);
|
||||
$stateRegistryProvider.register(config);
|
||||
|
@ -452,6 +472,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
$stateRegistryProvider.register(dashboard);
|
||||
$stateRegistryProvider.register(host);
|
||||
$stateRegistryProvider.register(hostBrowser);
|
||||
$stateRegistryProvider.register(hostJob);
|
||||
$stateRegistryProvider.register(events);
|
||||
$stateRegistryProvider.register(images);
|
||||
$stateRegistryProvider.register(image);
|
||||
|
@ -463,6 +484,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
$stateRegistryProvider.register(nodes);
|
||||
$stateRegistryProvider.register(node);
|
||||
$stateRegistryProvider.register(nodeBrowser);
|
||||
$stateRegistryProvider.register(nodeJob);
|
||||
$stateRegistryProvider.register(secrets);
|
||||
$stateRegistryProvider.register(secret);
|
||||
$stateRegistryProvider.register(secretCreation);
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.purgeAction()">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Clear job history
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-filters nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Id')">
|
||||
Id
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open">
|
||||
<a ng-click="$ctrl.changeOrderBy('Status')">
|
||||
State
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
<div>
|
||||
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.state.enabled"> Filter
|
||||
<i class="fa fa-filter" aria-hidden="true"></i></span>
|
||||
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.state.enabled">Filter
|
||||
<i class="fa fa-check" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
<div class="dropdown-menu" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader">
|
||||
Filter by state
|
||||
</div>
|
||||
<div class="menuContent">
|
||||
<div class="md-checkbox" ng-repeat="filter in $ctrl.filters.state.values track by $index">
|
||||
<input id="filter_state_{{ $index }}" type="checkbox" ng-model="filter.display" ng-change="$ctrl.onStateFilterChange()" />
|
||||
<label for="filter_state_{{ $index }}">{{ filter.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Created')">
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && $ctrl.state.reverseOrder"></i>
|
||||
Created
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))">
|
||||
<td>
|
||||
<a ui-sref="docker.containers.container.logs({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Id }}">
|
||||
{{ item.Id | truncate: 32}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) !== -1" class="label label-{{ item.Status|containerstatusbadge }} interactive"
|
||||
uib-tooltip="This container has a health check">{{ item.Status }}</span>
|
||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) === -1" class="label label-{{ item.Status|containerstatusbadge }}">
|
||||
{{ item.Status }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{item.Created | getisodatefromtimestamp}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="9" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="9" class="text-center text-muted">No jobs available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
angular.module('portainer.docker').component('jobsDatatable', {
|
||||
templateUrl: './jobsDatatable.html',
|
||||
controller: 'JobsDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,143 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.controller('JobsDatatableController', ['$q', '$state', 'PaginationService', 'DatatableService', 'ContainerService', 'ModalService', 'Notifications',
|
||||
function ($q, $state, PaginationService, DatatableService, ContainerService, ModalService, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
this.state = {
|
||||
orderBy: this.orderBy,
|
||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
||||
displayTextFilter: false
|
||||
};
|
||||
|
||||
this.filters = {
|
||||
state: {
|
||||
open: false,
|
||||
enabled: false,
|
||||
values: []
|
||||
}
|
||||
};
|
||||
|
||||
this.changeOrderBy = function (orderField) {
|
||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||
this.state.orderBy = orderField;
|
||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
||||
};
|
||||
|
||||
this.changePaginationLimit = function () {
|
||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
||||
};
|
||||
|
||||
this.applyFilters = function (value) {
|
||||
var container = value;
|
||||
var filters = ctrl.filters;
|
||||
for (var i = 0; i < filters.state.values.length; i++) {
|
||||
var filter = filters.state.values[i];
|
||||
if (container.Status === filter.label && filter.display) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.onStateFilterChange = function () {
|
||||
var filters = this.filters.state.values;
|
||||
var filtered = false;
|
||||
for (var i = 0; i < filters.length; i++) {
|
||||
var filter = filters[i];
|
||||
if (!filter.display) {
|
||||
filtered = true;
|
||||
}
|
||||
}
|
||||
this.filters.state.enabled = filtered;
|
||||
DatatableService.setDataTableFilters(this.tableKey, this.filters);
|
||||
};
|
||||
|
||||
this.prepareTableFromDataset = function () {
|
||||
var availableStateFilters = [];
|
||||
for (var i = 0; i < this.dataset.length; i++) {
|
||||
var item = this.dataset[i];
|
||||
availableStateFilters.push({
|
||||
label: item.Status,
|
||||
display: true
|
||||
});
|
||||
}
|
||||
this.filters.state.values = _.uniqBy(availableStateFilters, 'label');
|
||||
};
|
||||
|
||||
this.updateStoredFilters = function (storedFilters) {
|
||||
var datasetFilters = this.filters.state.values;
|
||||
|
||||
for (var i = 0; i < datasetFilters.length; i++) {
|
||||
var filter = datasetFilters[i];
|
||||
var existingFilter = _.find(storedFilters, ['label', filter.label]);
|
||||
if (existingFilter && !existingFilter.display) {
|
||||
filter.display = existingFilter.display;
|
||||
this.filters.state.enabled = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function confirmPurgeJobs() {
|
||||
return showConfirmationModal();
|
||||
|
||||
function showConfirmationModal() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure ?',
|
||||
message: 'Clearing job history will remove all stopped jobs containers.',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Purge',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
deferred.resolve(confirmed);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
}
|
||||
|
||||
this.purgeAction = function () {
|
||||
confirmPurgeJobs().then(function success(confirmed) {
|
||||
if (!confirmed) {
|
||||
return $q.when();
|
||||
}
|
||||
ContainerService.prune({ label: ['io.portainer.job.endpoint'] }).then(function success() {
|
||||
Notifications.success('Success', 'Job hisotry cleared');
|
||||
$state.reload();
|
||||
}).catch(function error(err) {
|
||||
Notifications.error('Failure', err.message, 'Unable to clear job history');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.$onInit = function () {
|
||||
setDefaults(this);
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
this.state.orderBy = storedOrder.orderBy;
|
||||
}
|
||||
|
||||
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||
if (storedFilters !== null) {
|
||||
this.updateStoredFilters(storedFilters.state.values);
|
||||
}
|
||||
this.filters.state.open = false;
|
||||
};
|
||||
|
||||
function setDefaults(ctrl) {
|
||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
||||
}
|
||||
}
|
||||
]);
|
|
@ -11,14 +11,25 @@
|
|||
<rd-header-content>Docker</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<host-details-panel
|
||||
host="$ctrl.hostDetails"
|
||||
is-agent="$ctrl.isAgent"
|
||||
browse-url="{{$ctrl.browseUrl}}"></host-details-panel>
|
||||
<host-details-panel
|
||||
host="$ctrl.hostDetails"
|
||||
is-browse-enabled="$ctrl.isAgent && $ctrl.agentApiVersion > 1"
|
||||
browse-url="{{$ctrl.browseUrl}}"
|
||||
is-job-enabled="$ctrl.isJobEnabled"
|
||||
job-url="{{$ctrl.jobUrl}}"
|
||||
></host-details-panel>
|
||||
|
||||
<engine-details-panel engine="$ctrl.engineDetails"></engine-details-panel>
|
||||
|
||||
<devices-panel ng-if="$ctrl.isAgent" devices="$ctrl.devices"></devices-panel>
|
||||
<disks-panel ng-if="$ctrl.isAgent" disks="$ctrl.disks"></disks-panel>
|
||||
<jobs-datatable
|
||||
ng-if="$ctrl.isJobEnabled && $ctrl.jobs"
|
||||
title-text="Jobs" title-icon="fa-tasks"
|
||||
dataset="$ctrl.jobs"
|
||||
table-key="jobs"
|
||||
order-by="Created" reverse-order="true"
|
||||
></jobs-datatable>
|
||||
|
||||
<ng-transclude></ng-transclude>
|
||||
<devices-panel ng-if="$ctrl.isAgent && $ctrl.agentApiVersion > 1" devices="$ctrl.devices"></devices-panel>
|
||||
<disks-panel ng-if="$ctrl.isAgent && $ctrl.agentApiVersion > 1" disks="$ctrl.disks"></disks-panel>
|
||||
|
||||
<ng-transclude></ng-transclude>
|
||||
|
|
|
@ -6,8 +6,12 @@ angular.module('portainer.docker').component('hostOverview', {
|
|||
devices: '<',
|
||||
disks: '<',
|
||||
isAgent: '<',
|
||||
agentApiVersion: '<',
|
||||
refreshUrl: '@',
|
||||
browseUrl: '@'
|
||||
browseUrl: '@',
|
||||
jobUrl: '@',
|
||||
isJobEnabled: '<',
|
||||
jobs: '<'
|
||||
},
|
||||
transclude: true
|
||||
});
|
||||
|
|
|
@ -26,20 +26,19 @@
|
|||
<td>Total memory</td>
|
||||
<td>{{ $ctrl.host.totalMemory | humansize }}</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.isAgent">
|
||||
<tr ng-if="$ctrl.isBrowseEnabled || $ctrl.isJobEnabled">
|
||||
<td colspan="2">
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
title="Browse"
|
||||
ui-sref="{{$ctrl.browseUrl}}">
|
||||
<button class="btn btn-primary btn-sm" title="Browse" ng-if="$ctrl.isBrowseEnabled" ui-sref="{{$ctrl.browseUrl}}">
|
||||
Browse
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" title="Execute job" ng-if="$ctrl.isJobEnabled" ui-sref="{{$ctrl.jobUrl}}">
|
||||
Execute job
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
angular.module('portainer.docker').component('hostDetailsPanel', {
|
||||
templateUrl:
|
||||
'app/docker/components/host-view-panels/host-details-panel/host-details-panel.html',
|
||||
templateUrl: './host-details-panel.html',
|
||||
bindings: {
|
||||
host: '<',
|
||||
isAgent: '<',
|
||||
browseUrl: '@'
|
||||
isJobEnabled: '<',
|
||||
isBrowseEnabled: '<',
|
||||
browseUrl: '@',
|
||||
jobUrl: '@'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -67,7 +67,8 @@ export function ContainerViewModel(data) {
|
|||
|
||||
export function ContainerStatsViewModel(data) {
|
||||
this.Date = data.read;
|
||||
this.MemoryUsage = data.memory_stats.usage;
|
||||
this.MemoryUsage = data.memory_stats.usage - data.memory_stats.stats.cache;
|
||||
this.MemoryCache = data.memory_stats.stats.cache;
|
||||
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;
|
||||
|
|
|
@ -71,6 +71,9 @@ function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
|||
},
|
||||
update: {
|
||||
method: 'POST', params: { id: '@id', action: 'update'}
|
||||
},
|
||||
prune: {
|
||||
method: 'POST', params: { action: 'prune', filters: '@filters' }
|
||||
}
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -189,5 +189,9 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe
|
|||
return Container.inspect({ id: id }).$promise;
|
||||
};
|
||||
|
||||
service.prune = function(filters) {
|
||||
return Container.prune({ filters: filters }).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -6,8 +6,8 @@ import { ContainerDetailsViewModel } from '../../../models/container';
|
|||
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.controller('CreateContainerController', ['$q', '$scope', '$state', '$timeout', '$transition$', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', 'SystemService', 'SettingsService', 'HttpRequestHelper',
|
||||
function ($q, $scope, $state, $timeout, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService, SystemService, SettingsService, HttpRequestHelper) {
|
||||
.controller('CreateContainerController', ['$q', '$scope', '$state', '$timeout', '$transition$', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', 'SystemService', 'SettingsService', 'PluginService', 'HttpRequestHelper',
|
||||
function ($q, $scope, $state, $timeout, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService, SystemService, SettingsService, PluginService, HttpRequestHelper) {
|
||||
|
||||
$scope.create = create;
|
||||
|
||||
|
@ -26,7 +26,9 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
MemoryLimit: 0,
|
||||
MemoryReservation: 0,
|
||||
NodeName: null,
|
||||
capabilities: []
|
||||
capabilities: [],
|
||||
LogDriverName: '',
|
||||
LogDriverOpts: []
|
||||
};
|
||||
|
||||
$scope.extraNetworks = {};
|
||||
|
@ -117,6 +119,14 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
$scope.config.HostConfig.Devices.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addLogDriverOpt = function() {
|
||||
$scope.formValues.LogDriverOpts.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLogDriverOpt = function(index) {
|
||||
$scope.formValues.LogDriverOpts.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.fromContainerMultipleNetworks = false;
|
||||
|
||||
function prepareImageConfig(config) {
|
||||
|
@ -264,6 +274,23 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
}
|
||||
}
|
||||
|
||||
function prepareLogDriver(config) {
|
||||
var logOpts = {};
|
||||
if ($scope.formValues.LogDriverName) {
|
||||
config.HostConfig.LogConfig = { Type: $scope.formValues.LogDriverName };
|
||||
if ($scope.formValues.LogDriverName !== 'none') {
|
||||
$scope.formValues.LogDriverOpts.forEach(function (opt) {
|
||||
if (opt.name) {
|
||||
logOpts[opt.name] = opt.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) {
|
||||
config.HostConfig.LogConfig.Config = logOpts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepareCapabilities(config) {
|
||||
var allowed = $scope.formValues.capabilities.filter(function(item) {return item.allowed === true;});
|
||||
var notAllowed = $scope.formValues.capabilities.filter(function(item) {return item.allowed === false;});
|
||||
|
@ -285,6 +312,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
prepareLabels(config);
|
||||
prepareDevices(config);
|
||||
prepareResources(config);
|
||||
prepareLogDriver(config);
|
||||
prepareCapabilities(config);
|
||||
return config;
|
||||
}
|
||||
|
@ -575,6 +603,11 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
||||
});
|
||||
|
||||
PluginService.loggingPlugins(apiVersion < 1.25)
|
||||
.then(function success(loggingDrivers) {
|
||||
$scope.availableLoggingDrivers = loggingDrivers;
|
||||
});
|
||||
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
$scope.isAdmin = userDetails.role === 1;
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@
|
|||
<rd-widget-header icon="fa-cog" title-text="Advanced container settings"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<ul class="nav nav-pills nav-justified">
|
||||
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li>
|
||||
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command & logging</a></li>
|
||||
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
|
||||
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
<li class="interactive"><a data-target="#env" data-toggle="tab">Env</a></li>
|
||||
|
@ -162,7 +162,7 @@
|
|||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- command-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label>
|
||||
<label for="container_command" class="col-sm-2 col-lg-1 control-label text-left">Command & logging</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="config.Cmd" id="container_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
|
||||
</div>
|
||||
|
@ -221,6 +221,59 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !console -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Logging
|
||||
</div>
|
||||
<!-- logging-driver -->
|
||||
<div class="form-group">
|
||||
<label for="log-driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
|
||||
<div class="col-sm-4">
|
||||
<select class="form-control" ng-model="formValues.LogDriverName" id="log-driver">
|
||||
<option selected value="">Default logging driver</option>
|
||||
<option ng-repeat="driver in availableLoggingDrivers" ng-value="driver">{{ driver }}</option>
|
||||
<option value="none">none</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Logging driver that will override the default docker daemon driver. Select Default logging driver if you don't want to override it. Supported logging drivers can be found <a href="https://docs.docker.com/engine/admin/logging/overview/#supported-logging-drivers" target="_blank">in the Docker documentation</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !logging-driver -->
|
||||
<!-- logging-opts -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">
|
||||
Options
|
||||
<portainer-tooltip position="top" message="Add button is disabled unless a driver other than none or default is selected. Options are specific to the selected driver, refer to the driver documentation."></portainer-tooltip>
|
||||
</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="!formValues.LogDriverName || formValues.LogDriverName === 'none' || addLogDriverOpt(formValues.LogDriverName)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add logging driver option
|
||||
</span>
|
||||
</div>
|
||||
<!-- logging-opts-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="opt in formValues.LogDriverOpts" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">option</span>
|
||||
<input type="text" class="form-control" ng-model="opt.name" placeholder="e.g. FOO">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="opt.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLogDriverOpt($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- logging-opts-input-list -->
|
||||
</div>
|
||||
<!-- !logging-opts -->
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-command -->
|
||||
|
|
|
@ -34,9 +34,8 @@ function ($q, $scope, $transition$, $document, $interval, ContainerService, Char
|
|||
|
||||
function updateMemoryChart(stats, chart) {
|
||||
var label = moment(stats.Date).format('HH:mm:ss');
|
||||
var value = stats.MemoryUsage;
|
||||
|
||||
ChartService.UpdateMemoryChart(label, value, chart);
|
||||
ChartService.UpdateMemoryChart(label, stats.MemoryUsage, stats.MemoryCache, chart);
|
||||
}
|
||||
|
||||
function updateCPUChart(stats, chart) {
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
angular
|
||||
.module('portainer.docker')
|
||||
.controller('HostBrowserViewController', [
|
||||
'SystemService', 'HttpRequestHelper',
|
||||
function HostBrowserViewController(SystemService, HttpRequestHelper) {
|
||||
var ctrl = this;
|
||||
angular.module('portainer.docker').controller('HostBrowserViewController', [
|
||||
'SystemService', 'Notifications',
|
||||
function HostBrowserViewController(SystemService, Notifications) {
|
||||
var ctrl = this;
|
||||
ctrl.$onInit = $onInit;
|
||||
|
||||
ctrl.$onInit = $onInit;
|
||||
|
||||
function $onInit() {
|
||||
loadInfo();
|
||||
}
|
||||
|
||||
function loadInfo() {
|
||||
SystemService.info().then(function onInfoLoaded(host) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(host.Name);
|
||||
ctrl.host = host;
|
||||
});
|
||||
}
|
||||
function $onInit() {
|
||||
SystemService.info()
|
||||
.then(function onInfoLoaded(host) {
|
||||
ctrl.host = host;
|
||||
})
|
||||
.catch(function onError(err) {
|
||||
Notifications.error('Unable to retrieve host information', err);
|
||||
});
|
||||
}
|
||||
]);
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
angular.module('portainer.docker').controller('HostJobController', [
|
||||
'SystemService', 'Notifications',
|
||||
function HostJobController(SystemService, Notifications) {
|
||||
var ctrl = this;
|
||||
ctrl.$onInit = $onInit;
|
||||
|
||||
function $onInit() {
|
||||
SystemService.info()
|
||||
.then(function onInfoLoaded(host) {
|
||||
ctrl.host = host;
|
||||
})
|
||||
.catch(function onError(err) {
|
||||
Notifications.error('Unable to retrieve host information', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
|
@ -0,0 +1,16 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Host job execution"></rd-header-title>
|
||||
<rd-header-content>
|
||||
Host > <a ui-sref="docker.host">{{ $ctrl.host.Name }}</a> > execute job
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<execute-job-form></execute-job-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
angular.module('portainer.docker').component('hostJobView', {
|
||||
templateUrl: './host-job.html',
|
||||
controller: 'HostJobController'
|
||||
});
|
|
@ -1,44 +1,53 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.docker').controller('HostViewController', [
|
||||
'$q', 'SystemService', 'Notifications', 'StateManager', 'AgentService',
|
||||
function HostViewController($q, SystemService, Notifications, StateManager, AgentService) {
|
||||
'$q', 'SystemService', 'Notifications', 'StateManager', 'AgentService', 'ContainerService', 'Authentication',
|
||||
function HostViewController($q, SystemService, Notifications, StateManager, AgentService, ContainerService, Authentication) {
|
||||
var ctrl = this;
|
||||
|
||||
this.$onInit = initView;
|
||||
|
||||
ctrl.state = {
|
||||
isAgent: false
|
||||
isAgent: false,
|
||||
isAdmin : false
|
||||
};
|
||||
|
||||
|
||||
this.engineDetails = {};
|
||||
this.hostDetails = {};
|
||||
this.devices = null;
|
||||
this.disks = null;
|
||||
|
||||
function initView() {
|
||||
var applicationState = StateManager.getState();
|
||||
ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy;
|
||||
ctrl.state.isAdmin = Authentication.getUserDetails().role === 1;
|
||||
var agentApiVersion = applicationState.endpoint.agentApiVersion;
|
||||
ctrl.state.agentApiVersion = agentApiVersion;
|
||||
|
||||
$q.all({
|
||||
version: SystemService.version(),
|
||||
info: SystemService.info()
|
||||
info: SystemService.info(),
|
||||
jobs: ctrl.state.isAdmin ? ContainerService.containers(true, { label: ['io.portainer.job.endpoint'] }) : []
|
||||
})
|
||||
.then(function success(data) {
|
||||
ctrl.engineDetails = buildEngineDetails(data);
|
||||
ctrl.hostDetails = buildHostDetails(data.info);
|
||||
.then(function success(data) {
|
||||
ctrl.engineDetails = buildEngineDetails(data);
|
||||
ctrl.hostDetails = buildHostDetails(data.info);
|
||||
ctrl.jobs = data.jobs;
|
||||
|
||||
if (ctrl.state.isAgent) {
|
||||
return AgentService.hostInfo(data.info.Hostname).then(function onHostInfoLoad(agentHostInfo) {
|
||||
ctrl.devices = agentHostInfo.PCIDevices;
|
||||
ctrl.disks = agentHostInfo.PhysicalDisks;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error(
|
||||
'Failure',
|
||||
err,
|
||||
'Unable to retrieve engine details'
|
||||
);
|
||||
});
|
||||
if (ctrl.state.isAgent && agentApiVersion > 1) {
|
||||
return AgentService.hostInfo(data.info.Hostname).then(function onHostInfoLoad(agentHostInfo) {
|
||||
ctrl.devices = agentHostInfo.PCIDevices;
|
||||
ctrl.disks = agentHostInfo.PhysicalDisks;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error(
|
||||
'Failure',
|
||||
err,
|
||||
'Unable to retrieve engine details'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function buildEngineDetails(data) {
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
engine-details="$ctrl.engineDetails"
|
||||
host-details="$ctrl.hostDetails"
|
||||
is-agent="$ctrl.state.isAgent"
|
||||
agent-api-version="$ctrl.state.agentApiVersion"
|
||||
disks="$ctrl.disks"
|
||||
devices="$ctrl.devices"
|
||||
|
||||
refresh-url="docker.host"
|
||||
browse-url="docker.host.browser"
|
||||
></host-overview>
|
||||
is-job-enabled="$ctrl.state.isAdmin"
|
||||
job-url="docker.host.job"
|
||||
jobs="$ctrl.jobs"
|
||||
></host-overview>
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.docker').controller('NodeBrowserController', [
|
||||
'NodeService', 'HttpRequestHelper', '$stateParams',
|
||||
function NodeBrowserController(NodeService, HttpRequestHelper, $stateParams) {
|
||||
'$stateParams', 'NodeService', 'HttpRequestHelper', 'Notifications',
|
||||
function NodeBrowserController($stateParams, NodeService, HttpRequestHelper, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.$onInit = $onInit;
|
||||
|
||||
function $onInit() {
|
||||
ctrl.nodeId = $stateParams.id;
|
||||
loadNode();
|
||||
}
|
||||
|
||||
function loadNode() {
|
||||
NodeService.node(ctrl.nodeId).then(function onNodeLoaded(node) {
|
||||
NodeService.node(ctrl.nodeId)
|
||||
.then(function onNodeLoaded(node) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(node.Hostname);
|
||||
ctrl.node = node;
|
||||
})
|
||||
.catch(function onError(err) {
|
||||
Notifications.error('Unable to retrieve host information', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,45 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.docker').controller('NodeDetailsViewController', [
|
||||
'$stateParams', 'NodeService', 'StateManager', 'AgentService',
|
||||
function NodeDetailsViewController($stateParams, NodeService, StateManager, AgentService) {
|
||||
'$q', '$stateParams', 'NodeService', 'StateManager', 'AgentService', 'ContainerService', 'Authentication',
|
||||
function NodeDetailsViewController($q, $stateParams, NodeService, StateManager, AgentService, ContainerService, Authentication) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.$onInit = initView;
|
||||
|
||||
ctrl.state = {
|
||||
isAgent: false
|
||||
isAgent: false,
|
||||
isAdmin: false
|
||||
};
|
||||
|
||||
function initView() {
|
||||
var applicationState = StateManager.getState();
|
||||
ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy;
|
||||
ctrl.state.isAdmin = Authentication.getUserDetails().role === 1;
|
||||
|
||||
var fetchJobs = ctrl.state.isAdmin && ctrl.state.isAgent;
|
||||
|
||||
var nodeId = $stateParams.id;
|
||||
NodeService.node(nodeId).then(function(node) {
|
||||
$q.all({
|
||||
node: NodeService.node(nodeId),
|
||||
jobs: fetchJobs ? ContainerService.containers(true, { label: ['io.portainer.job.endpoint'] }) : []
|
||||
})
|
||||
.then(function (data) {
|
||||
var node = data.node;
|
||||
ctrl.originalNode = node;
|
||||
ctrl.hostDetails = buildHostDetails(node);
|
||||
ctrl.engineDetails = buildEngineDetails(node);
|
||||
ctrl.nodeDetails = buildNodeDetails(node);
|
||||
ctrl.jobs = data.jobs;
|
||||
if (ctrl.state.isAgent) {
|
||||
AgentService.hostInfo(node.Hostname).then(function onHostInfoLoad(
|
||||
agentHostInfo
|
||||
) {
|
||||
var agentApiVersion = applicationState.endpoint.agentApiVersion;
|
||||
ctrl.state.agentApiVersion = agentApiVersion;
|
||||
if (agentApiVersion < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
AgentService.hostInfo(node.Hostname)
|
||||
.then(function onHostInfoLoad(agentHostInfo) {
|
||||
ctrl.devices = agentHostInfo.PCIDevices;
|
||||
ctrl.disks = agentHostInfo.PhysicalDisks;
|
||||
});
|
||||
|
@ -66,12 +81,12 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [
|
|||
|
||||
function transformPlugins(pluginsList, type) {
|
||||
return pluginsList
|
||||
.filter(function(plugin) {
|
||||
return plugin.Type === type;
|
||||
})
|
||||
.map(function(plugin) {
|
||||
return plugin.Name;
|
||||
});
|
||||
.filter(function(plugin) {
|
||||
return plugin.Type === type;
|
||||
})
|
||||
.map(function(plugin) {
|
||||
return plugin.Name;
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
<host-overview
|
||||
agent-api-version="$ctrl.state.agentApiVersion"
|
||||
is-agent="$ctrl.state.isAgent"
|
||||
host-details="$ctrl.hostDetails"
|
||||
engine-details="$ctrl.engineDetails"
|
||||
disks="$ctrl.disks"
|
||||
devices="$ctrl.devices"
|
||||
|
||||
refresh-url="docker.nodes.node"
|
||||
browse-url="docker.nodes.node.browse"
|
||||
is-job-enabled="$ctrl.state.isAdmin && $ctrl.state.isAgent"
|
||||
job-url="docker.nodes.node.job"
|
||||
jobs="$ctrl.jobs"
|
||||
>
|
||||
<swarm-node-details-panel
|
||||
details="$ctrl.nodeDetails"
|
||||
original-node="$ctrl.originalNode"
|
||||
></swarm-node-details-panel>
|
||||
</host-overview>
|
||||
</host-overview>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
angular.module('portainer.docker').controller('NodeJobController', [
|
||||
'$stateParams', 'NodeService', 'HttpRequestHelper', 'Notifications',
|
||||
function NodeJobController($stateParams, NodeService, HttpRequestHelper, Notifications) {
|
||||
var ctrl = this;
|
||||
ctrl.$onInit = $onInit;
|
||||
|
||||
function $onInit() {
|
||||
ctrl.nodeId = $stateParams.id;
|
||||
|
||||
NodeService.node(ctrl.nodeId)
|
||||
.then(function onNodeLoaded(node) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(node.Hostname);
|
||||
ctrl.node = node;
|
||||
})
|
||||
.catch(function onError(err) {
|
||||
Notifications.error('Unable to retrieve host information', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
|
@ -0,0 +1,18 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Host job execution"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="docker.swarm">Swarm</a> > <a ui-sref="docker.nodes.node({ id: $ctrl.nodeId })">{{ $ctrl.node.Hostname }}</a> > execute job
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<execute-job-form
|
||||
node-name="$ctrl.node.Hostname"
|
||||
></execute-job-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
angular.module('portainer.docker').component('nodeJobView', {
|
||||
templateUrl: './node-job.html',
|
||||
controller: 'NodeJobController'
|
||||
});
|
|
@ -10,7 +10,7 @@
|
|||
<div ng-repeat="config in formValues.Configs" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">config</span>
|
||||
<select class="form-control" ng-model="config.model" ng-options="config.Name for config in availableConfigs">
|
||||
<select class="form-control" ng-model="config.model" ng-options="config.Name for config in availableConfigs | orderBy: 'Name'">
|
||||
<option value="" selected="selected">Select a config</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div ng-repeat="secret in formValues.Secrets track by $index" style="margin-top: 4px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">secret</span>
|
||||
<select class="form-control" ng-model="secret.model" ng-options="secret.Name for secret in availableSecrets">
|
||||
<select class="form-control" ng-model="secret.model" ng-options="secret.Name for secret in availableSecrets | orderBy: 'Name'">
|
||||
<option value="" selected="selected">Select a secret</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px;">
|
||||
Add a config:
|
||||
<select class="form-control" ng-options="config.Name for config in configs" ng-model="newConfig">
|
||||
<select class="form-control" ng-options="config.Name for config in configs | orderBy: 'Name'" ng-model="newConfig">
|
||||
<option selected disabled hidden value="">Select a config</option>
|
||||
</select>
|
||||
<a class="btn btn-default btn-sm" ng-click="addConfig(service, newConfig)">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px;">
|
||||
Add a secret:
|
||||
<select class="form-control" ng-options="secret.Name for secret in secrets" ng-model="state.addSecret.secret">
|
||||
<select class="form-control" ng-options="secret.Name for secret in secrets | orderBy: 'Name'" ng-model="state.addSecret.secret">
|
||||
<option selected disabled hidden value="">Select a secret</option>
|
||||
</select>
|
||||
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.30 && state.addSecret.override">
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.controller('BrowseVolumeController', ['$scope', '$transition$',
|
||||
function ($scope, $transition$) {
|
||||
.controller('BrowseVolumeController', ['$scope', '$transition$', 'StateManager',
|
||||
function ($scope, $transition$, StateManager) {
|
||||
|
||||
function initView() {
|
||||
$scope.volumeId = $transition$.params().id;
|
||||
$scope.nodeName = $transition$.params().nodeName;
|
||||
$scope.agentApiVersion = StateManager.getAgentApiVersion();
|
||||
}
|
||||
|
||||
initView();
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
<volume-browser
|
||||
volume-id="volumeId"
|
||||
node-name="nodeName"
|
||||
|
||||
is-upload-enabled="agentApiVersion > 1"
|
||||
></volume-browser>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('JobFormController', ['$state', 'LocalStorage', 'EndpointService', 'EndpointProvider', 'Notifications',
|
||||
function ($state, LocalStorage, EndpointService, EndpointProvider, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.$onInit = onInit;
|
||||
ctrl.editorUpdate = editorUpdate;
|
||||
ctrl.executeJob = executeJob;
|
||||
|
||||
ctrl.state = {
|
||||
Method: 'editor',
|
||||
formValidationError: '',
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
ctrl.formValues = {
|
||||
Image: 'ubuntu:latest',
|
||||
JobFileContent: '',
|
||||
JobFile: null
|
||||
};
|
||||
|
||||
function onInit() {
|
||||
var storedImage = LocalStorage.getJobImage();
|
||||
if (storedImage) {
|
||||
ctrl.formValues.Image = storedImage;
|
||||
}
|
||||
}
|
||||
|
||||
function editorUpdate(cm) {
|
||||
ctrl.formValues.JobFileContent = cm.getValue();
|
||||
}
|
||||
|
||||
function createJob(image, method) {
|
||||
var endpointId = EndpointProvider.endpointID();
|
||||
var nodeName = ctrl.nodeName;
|
||||
|
||||
if (method === 'editor') {
|
||||
var jobFileContent = ctrl.formValues.JobFileContent;
|
||||
return EndpointService.executeJobFromFileContent(image, jobFileContent, endpointId, nodeName);
|
||||
}
|
||||
|
||||
var jobFile = ctrl.formValues.JobFile;
|
||||
return EndpointService.executeJobFromFileUpload(image, jobFile, endpointId, nodeName);
|
||||
}
|
||||
|
||||
function executeJob() {
|
||||
var method = ctrl.state.Method;
|
||||
if (method === 'editor' && ctrl.formValues.JobFileContent === '') {
|
||||
ctrl.state.formValidationError = 'Script file content must not be empty';
|
||||
return;
|
||||
}
|
||||
|
||||
var image = ctrl.formValues.Image;
|
||||
LocalStorage.storeJobImage(image);
|
||||
|
||||
ctrl.state.actionInProgress = true;
|
||||
createJob(image, method)
|
||||
.then(function success() {
|
||||
Notifications.success('Job successfully created');
|
||||
$state.go('^');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Job execution failure', err);
|
||||
})
|
||||
.finally(function final() {
|
||||
ctrl.state.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
}]);
|
|
@ -0,0 +1,110 @@
|
|||
<form class="form-horizontal" name="executeJobForm">
|
||||
<!-- image-input -->
|
||||
<div class="form-group">
|
||||
<label for="job_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.formValues.Image" id="job_image" name="job_image" placeholder="e.g. ubuntu:latest" required auto-focus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="executeJobForm.job_image.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="executeJobForm.job_image.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !image-input -->
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
This job will run inside a privileged container on the host. You can access the host filesystem under the
|
||||
<code>/host</code> folder.
|
||||
</span>
|
||||
</div>
|
||||
<!-- execution-method -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Job creation
|
||||
</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="$ctrl.state.Method" value="editor">
|
||||
<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="$ctrl.state.Method" value="upload">
|
||||
<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 from your computer</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !execution-method -->
|
||||
<!-- web-editor -->
|
||||
<div ng-show="$ctrl.state.Method === 'editor'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Web editor
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<code-editor
|
||||
identifier="execute-job-editor"
|
||||
placeholder="# Define or paste the content of your script file here"
|
||||
on-change="$ctrl.editorUpdate">
|
||||
</code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !web-editor -->
|
||||
<!-- upload -->
|
||||
<div ng-show="$ctrl.state.Method === '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 script file from your computer.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formValues.JobFile">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ $ctrl.formValues.JobFile.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!$ctrl.formValues.JobFile" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !upload -->
|
||||
<!-- 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="$ctrl.state.actionInProgress || !executeJobForm.$valid
|
||||
|| ($ctrl.state.Method === 'upload' && !$ctrl.formValues.JobFile)"
|
||||
ng-click="$ctrl.executeJob()"
|
||||
button-spinner="$ctrl.state.actionInProgress">
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Execute</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Starting job...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px;">
|
||||
{{ $ctrl.state.formValidationError }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
|
@ -0,0 +1,7 @@
|
|||
angular.module('portainer.app').component('executeJobForm', {
|
||||
templateUrl: './execute-job-form.html',
|
||||
controller: 'JobFormController',
|
||||
bindings: {
|
||||
nodeName: '<'
|
||||
}
|
||||
});
|
|
@ -9,6 +9,7 @@ angular.module('portainer.app')
|
|||
update: { method: 'PUT', params: { id: '@id' } },
|
||||
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
|
||||
remove: { method: 'DELETE', params: { id: '@id'} },
|
||||
snapshot: { method: 'POST', params: { id: 'snapshot' }}
|
||||
snapshot: { method: 'POST', params: { id: 'snapshot' }},
|
||||
executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } }
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -102,5 +102,18 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.executeJobFromFileUpload = function (image, jobFile, endpointId, nodeName) {
|
||||
return FileUploadService.executeEndpointJob(image, jobFile, endpointId, nodeName);
|
||||
};
|
||||
|
||||
service.executeJobFromFileContent = function (image, jobFileContent, endpointId, nodeName) {
|
||||
var payload = {
|
||||
Image: image,
|
||||
FileContent: jobFileContent
|
||||
};
|
||||
|
||||
return Endpoints.executeJob({ id: endpointId, method: 'string', nodeName: nodeName }, payload).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -4,43 +4,59 @@ angular.module('portainer.app')
|
|||
.factory('Authentication', ['$q', 'Auth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', function AuthenticationFactory($q, Auth, jwtHelper, LocalStorage, StateManager, EndpointProvider) {
|
||||
'use strict';
|
||||
|
||||
var service = {};
|
||||
var user = {};
|
||||
return {
|
||||
init: function() {
|
||||
var jwt = LocalStorage.getJWT();
|
||||
if (jwt) {
|
||||
var tokenPayload = jwtHelper.decodeToken(jwt);
|
||||
user.username = tokenPayload.username;
|
||||
user.ID = tokenPayload.id;
|
||||
user.role = tokenPayload.role;
|
||||
}
|
||||
},
|
||||
login: function(username, password) {
|
||||
return $q(function (resolve, reject) {
|
||||
Auth.login({username: username, password: password}).$promise
|
||||
.then(function(data) {
|
||||
LocalStorage.storeJWT(data.jwt);
|
||||
var tokenPayload = jwtHelper.decodeToken(data.jwt);
|
||||
user.username = username;
|
||||
user.ID = tokenPayload.id;
|
||||
user.role = tokenPayload.role;
|
||||
resolve();
|
||||
}, function() {
|
||||
reject();
|
||||
});
|
||||
});
|
||||
},
|
||||
logout: function() {
|
||||
StateManager.clean();
|
||||
EndpointProvider.clean();
|
||||
LocalStorage.clean();
|
||||
},
|
||||
isAuthenticated: function() {
|
||||
var jwt = LocalStorage.getJWT();
|
||||
return jwt && !jwtHelper.isTokenExpired(jwt);
|
||||
},
|
||||
getUserDetails: function() {
|
||||
return user;
|
||||
|
||||
service.init = init;
|
||||
service.login = login;
|
||||
service.logout = logout;
|
||||
service.isAuthenticated = isAuthenticated;
|
||||
service.getUserDetails = getUserDetails;
|
||||
|
||||
function init() {
|
||||
var jwt = LocalStorage.getJWT();
|
||||
|
||||
if (jwt) {
|
||||
var tokenPayload = jwtHelper.decodeToken(jwt);
|
||||
user.username = tokenPayload.username;
|
||||
user.ID = tokenPayload.id;
|
||||
user.role = tokenPayload.role;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function login(username, password) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Auth.login({username: username, password: password}).$promise
|
||||
.then(function success(data) {
|
||||
LocalStorage.storeJWT(data.jwt);
|
||||
var tokenPayload = jwtHelper.decodeToken(data.jwt);
|
||||
user.username = username;
|
||||
user.ID = tokenPayload.id;
|
||||
user.role = tokenPayload.role;
|
||||
deferred.resolve();
|
||||
})
|
||||
.catch(function error() {
|
||||
deferred.reject();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function logout() {
|
||||
StateManager.clean();
|
||||
EndpointProvider.clean();
|
||||
LocalStorage.clean();
|
||||
}
|
||||
|
||||
function isAuthenticated() {
|
||||
var jwt = LocalStorage.getJWT();
|
||||
return jwt && !jwtHelper.isTokenExpired(jwt);
|
||||
}
|
||||
|
||||
function getUserDetails() {
|
||||
return user;
|
||||
}
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -3,16 +3,16 @@ import Chart from 'chart.js';
|
|||
import filesize from 'filesize';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.factory('ChartService', [function ChartService() {
|
||||
'use strict';
|
||||
.factory('ChartService', [function ChartService() {
|
||||
'use strict';
|
||||
|
||||
// Max. number of items to display on a chart
|
||||
var CHART_LIMIT = 600;
|
||||
// Max. number of items to display on a chart
|
||||
var CHART_LIMIT = 600;
|
||||
|
||||
var service = {};
|
||||
var service = {};
|
||||
|
||||
function defaultChartOptions(pos, tooltipCallback, scalesCallback) {
|
||||
return {
|
||||
function defaultChartOptions(pos, tooltipCallback, scalesCallback, isStacked) {
|
||||
return {
|
||||
animation: { duration: 0 },
|
||||
responsiveAnimationDuration: 0,
|
||||
responsive: true,
|
||||
|
@ -21,7 +21,7 @@ angular.module('portainer.app')
|
|||
intersect: false,
|
||||
position: pos,
|
||||
callbacks: {
|
||||
label: function(tooltipItem, data) {
|
||||
label: function (tooltipItem, data) {
|
||||
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label;
|
||||
return tooltipCallback(datasetLabel, tooltipItem.yLabel);
|
||||
}
|
||||
|
@ -30,142 +30,188 @@ angular.module('portainer.app')
|
|||
hover: { animationDuration: 0 },
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: scalesCallback
|
||||
}
|
||||
stacked: isStacked,
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: scalesCallback
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function CreateChart (context, label, tooltipCallback, scalesCallback) {
|
||||
return new Chart(context, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: label,
|
||||
data: [],
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||
borderColor: 'rgba(151,187,205,0.6)',
|
||||
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||
pointBorderColor: 'rgba(151,187,205,1)',
|
||||
pointRadius: 2,
|
||||
borderWidth: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
options: defaultChartOptions('nearest', tooltipCallback, scalesCallback)
|
||||
});
|
||||
}
|
||||
|
||||
service.CreateCPUChart = function(context) {
|
||||
return CreateChart(context, 'CPU', percentageBasedTooltipLabel, percentageBasedAxisLabel);
|
||||
};
|
||||
|
||||
service.CreateMemoryChart = function(context) {
|
||||
return CreateChart(context, 'Memory', byteBasedTooltipLabel, byteBasedAxisLabel);
|
||||
};
|
||||
|
||||
service.CreateNetworkChart = function(context) {
|
||||
return new Chart(context, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'RX on eth0',
|
||||
data: [],
|
||||
fill: false,
|
||||
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||
borderColor: 'rgba(151,187,205,0.6)',
|
||||
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||
pointBorderColor: 'rgba(151,187,205,1)',
|
||||
pointRadius: 2,
|
||||
borderWidth: 2
|
||||
},
|
||||
{
|
||||
label: 'TX on eth0',
|
||||
data: [],
|
||||
fill: false,
|
||||
backgroundColor: 'rgba(255,180,174,0.4)',
|
||||
borderColor: 'rgba(255,180,174,0.6)',
|
||||
pointBackgroundColor: 'rgba(255,180,174,1)',
|
||||
pointBorderColor: 'rgba(255,180,174,1)',
|
||||
pointRadius: 2,
|
||||
borderWidth: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
options: defaultChartOptions('average', byteBasedTooltipLabel, byteBasedAxisLabel)
|
||||
});
|
||||
};
|
||||
|
||||
function UpdateChart(label, value, chart) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(value);
|
||||
|
||||
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||
chart.data.labels.pop();
|
||||
chart.data.datasets[0].data.pop();
|
||||
}
|
||||
|
||||
chart.update(0);
|
||||
}
|
||||
|
||||
service.UpdateMemoryChart = UpdateChart;
|
||||
service.UpdateCPUChart = UpdateChart;
|
||||
|
||||
service.UpdateNetworkChart = function(label, rx, tx, chart) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(rx);
|
||||
chart.data.datasets[1].data.push(tx);
|
||||
|
||||
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||
chart.data.labels.pop();
|
||||
chart.data.datasets[0].data.pop();
|
||||
chart.data.datasets[1].data.pop();
|
||||
function CreateChart(context, label, tooltipCallback, scalesCallback) {
|
||||
return new Chart(context, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: label,
|
||||
data: [],
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||
borderColor: 'rgba(151,187,205,0.6)',
|
||||
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||
pointBorderColor: 'rgba(151,187,205,1)',
|
||||
pointRadius: 2,
|
||||
borderWidth: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
options: defaultChartOptions('nearest', tooltipCallback, scalesCallback)
|
||||
});
|
||||
}
|
||||
|
||||
chart.update(0);
|
||||
};
|
||||
|
||||
function byteBasedTooltipLabel(label, value) {
|
||||
var processedValue = 0;
|
||||
if (value > 5) {
|
||||
processedValue = filesize(value, {base: 10, round: 1});
|
||||
} else {
|
||||
processedValue = value.toFixed(1) + 'B';
|
||||
function CreateMemoryChart(context, tooltipCallback, scalesCallback) {
|
||||
return new Chart(context, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Memory',
|
||||
data: [],
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||
borderColor: 'rgba(151,187,205,0.6)',
|
||||
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||
pointBorderColor: 'rgba(151,187,205,1)',
|
||||
pointRadius: 2,
|
||||
borderWidth: 2
|
||||
},
|
||||
{
|
||||
label: 'Cache',
|
||||
data: [],
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(255,180,174,0.4)',
|
||||
borderColor: 'rgba(255,180,174,0.6)',
|
||||
pointBackgroundColor: 'rgba(255,180,174,1)',
|
||||
pointBorderColor: 'rgba(255,180,174,1)',
|
||||
pointRadius: 2,
|
||||
borderWidth: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
options: defaultChartOptions('nearest', tooltipCallback, scalesCallback, true)
|
||||
});
|
||||
}
|
||||
return label + ': ' + processedValue;
|
||||
}
|
||||
|
||||
function byteBasedAxisLabel(value) {
|
||||
if (value > 5) {
|
||||
return filesize(value, {base: 10, round: 1});
|
||||
service.CreateCPUChart = function (context) {
|
||||
return CreateChart(context, 'CPU', percentageBasedTooltipLabel, percentageBasedAxisLabel);
|
||||
};
|
||||
|
||||
service.CreateMemoryChart = function (context) {
|
||||
return CreateMemoryChart(context, byteBasedTooltipLabel, byteBasedAxisLabel);
|
||||
};
|
||||
|
||||
service.CreateNetworkChart = function (context) {
|
||||
return new Chart(context, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'RX on eth0',
|
||||
data: [],
|
||||
fill: false,
|
||||
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||
borderColor: 'rgba(151,187,205,0.6)',
|
||||
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||
pointBorderColor: 'rgba(151,187,205,1)',
|
||||
pointRadius: 2,
|
||||
borderWidth: 2
|
||||
},
|
||||
{
|
||||
label: 'TX on eth0',
|
||||
data: [],
|
||||
fill: false,
|
||||
backgroundColor: 'rgba(255,180,174,0.4)',
|
||||
borderColor: 'rgba(255,180,174,0.6)',
|
||||
pointBackgroundColor: 'rgba(255,180,174,1)',
|
||||
pointBorderColor: 'rgba(255,180,174,1)',
|
||||
pointRadius: 2,
|
||||
borderWidth: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
options: defaultChartOptions('average', byteBasedTooltipLabel, byteBasedAxisLabel)
|
||||
});
|
||||
};
|
||||
|
||||
function UpdateChart(label, value, chart) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(value);
|
||||
|
||||
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||
chart.data.labels.pop();
|
||||
chart.data.datasets[0].data.pop();
|
||||
}
|
||||
|
||||
chart.update(0);
|
||||
}
|
||||
return value.toFixed(1) + 'B';
|
||||
}
|
||||
|
||||
function percentageBasedAxisLabel(value) {
|
||||
if (value > 1) {
|
||||
return Math.round(value) + '%';
|
||||
service.UpdateMemoryChart = function UpdateChart(label, memoryValue, cacheValue, chart) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(memoryValue);
|
||||
chart.data.datasets[1].data.push(cacheValue);
|
||||
|
||||
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||
chart.data.labels.pop();
|
||||
chart.data.datasets[0].data.pop();
|
||||
}
|
||||
|
||||
chart.update(0);
|
||||
};
|
||||
service.UpdateCPUChart = UpdateChart;
|
||||
|
||||
service.UpdateNetworkChart = function (label, rx, tx, chart) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(rx);
|
||||
chart.data.datasets[1].data.push(tx);
|
||||
|
||||
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||
chart.data.labels.pop();
|
||||
chart.data.datasets[0].data.pop();
|
||||
chart.data.datasets[1].data.pop();
|
||||
}
|
||||
|
||||
chart.update(0);
|
||||
};
|
||||
|
||||
function byteBasedTooltipLabel(label, value) {
|
||||
var processedValue = 0;
|
||||
if (value > 5) {
|
||||
processedValue = filesize(value, { base: 10, round: 1 });
|
||||
} else {
|
||||
processedValue = value.toFixed(1) + 'B';
|
||||
}
|
||||
return label + ': ' + processedValue;
|
||||
}
|
||||
return value.toFixed(1) + '%';
|
||||
}
|
||||
|
||||
function percentageBasedTooltipLabel(label, value) {
|
||||
var processedValue = 0;
|
||||
if (value > 1) {
|
||||
processedValue = Math.round(value);
|
||||
} else {
|
||||
processedValue = value.toFixed(1);
|
||||
function byteBasedAxisLabel(value) {
|
||||
if (value > 5) {
|
||||
return filesize(value, { base: 10, round: 1 });
|
||||
}
|
||||
return value.toFixed(1) + 'B';
|
||||
}
|
||||
return label + ': ' + processedValue + '%';
|
||||
}
|
||||
|
||||
return service;
|
||||
}]);
|
||||
function percentageBasedAxisLabel(value) {
|
||||
if (value > 1) {
|
||||
return Math.round(value) + '%';
|
||||
}
|
||||
return value.toFixed(1) + '%';
|
||||
}
|
||||
|
||||
function percentageBasedTooltipLabel(label, value) {
|
||||
var processedValue = 0;
|
||||
if (value > 1) {
|
||||
processedValue = Math.round(value);
|
||||
} else {
|
||||
processedValue = value.toFixed(1);
|
||||
}
|
||||
return label + ': ' + processedValue + '%';
|
||||
}
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -67,6 +67,17 @@ angular.module('portainer.app')
|
|||
});
|
||||
};
|
||||
|
||||
service.executeEndpointJob = function (imageName, file, endpointId, nodeName) {
|
||||
return Upload.upload({
|
||||
url: 'api/endpoints/' + endpointId + '/job?method=file&nodeName=' + nodeName,
|
||||
data: {
|
||||
File: file,
|
||||
Image: imageName
|
||||
},
|
||||
ignoreLoadingBar: true
|
||||
});
|
||||
};
|
||||
|
||||
service.createEndpoint = function(name, type, URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
return Upload.upload({
|
||||
url: 'api/endpoints',
|
||||
|
|
|
@ -91,6 +91,12 @@ angular.module('portainer.app')
|
|||
getColumnVisibilitySettings: function(key) {
|
||||
return localStorageService.get('col_visibility_' + key);
|
||||
},
|
||||
storeJobImage: function(data) {
|
||||
localStorageService.set('job_image', data);
|
||||
},
|
||||
getJobImage: function() {
|
||||
return localStorageService.get('job_image');
|
||||
},
|
||||
clean: function() {
|
||||
localStorageService.clearAll();
|
||||
}
|
||||
|
|
|
@ -16,7 +16,9 @@ angular.module('portainer.app')
|
|||
|
||||
service.error = function(title, e, fallbackText) {
|
||||
var msg = fallbackText;
|
||||
if (e.data && e.data.message) {
|
||||
if (e.data && e.data.details) {
|
||||
msg = e.data.details;
|
||||
} else if (e.data && e.data.message) {
|
||||
msg = e.data.message;
|
||||
} else if (e.message) {
|
||||
msg = e.message;
|
||||
|
|
|
@ -2,8 +2,8 @@ import angular from 'angular';
|
|||
import moment from 'moment';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', 'APPLICATION_CACHE_VALIDITY',
|
||||
function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService, APPLICATION_CACHE_VALIDITY) {
|
||||
.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', 'APPLICATION_CACHE_VALIDITY', 'AgentPingService',
|
||||
function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService, APPLICATION_CACHE_VALIDITY, AgentPingService) {
|
||||
'use strict';
|
||||
|
||||
var manager = {};
|
||||
|
@ -160,6 +160,14 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
|||
state.endpoint.name = name;
|
||||
state.endpoint.apiVersion = endpointAPIVersion;
|
||||
state.endpoint.extensions = assignExtensions(extensions);
|
||||
|
||||
if (endpointMode.agentProxy) {
|
||||
return AgentPingService.ping().then(function onPingSuccess(data) {
|
||||
state.endpoint.agentApiVersion = data.version;
|
||||
});
|
||||
}
|
||||
|
||||
}).then(function () {
|
||||
LocalStorage.storeEndpointState(state.endpoint);
|
||||
deferred.resolve();
|
||||
})
|
||||
|
@ -173,5 +181,9 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
manager.getAgentApiVersion = function getAgentApiVersion() {
|
||||
return state.endpoint.agentApiVersion;
|
||||
};
|
||||
|
||||
return manager;
|
||||
}]);
|
||||
|
|
Loading…
Reference in New Issue