mirror of https://github.com/portainer/portainer
feat(agent): add agent support (#1828)
parent
77a85bd385
commit
2327d696e0
|
@ -0,0 +1,20 @@
|
|||
package bolt
|
||||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
func (m *Migrator) updateEndpointsToVersion10() error {
|
||||
legacyEndpoints, err := m.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range legacyEndpoints {
|
||||
endpoint.Type = portainer.DockerEnvironment
|
||||
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -104,6 +104,14 @@ func (m *Migrator) Migrate() error {
|
|||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/461
|
||||
if m.CurrentDBVersion < 10 {
|
||||
err := m.updateEndpointsToVersion10()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -38,6 +38,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
|
||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
||||
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
|
||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
||||
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
|
||||
|
|
|
@ -9,6 +9,7 @@ const (
|
|||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLSVerify = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
|
|
|
@ -7,6 +7,7 @@ const (
|
|||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLSVerify = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/portainer/portainer/filesystem"
|
||||
"github.com/portainer/portainer/git"
|
||||
"github.com/portainer/portainer/http"
|
||||
"github.com/portainer/portainer/http/client"
|
||||
"github.com/portainer/portainer/jwt"
|
||||
"github.com/portainer/portainer/ldap"
|
||||
|
||||
|
@ -61,8 +62,8 @@ func initStore(dataStorePath string) *bolt.Store {
|
|||
return store
|
||||
}
|
||||
|
||||
func initStackManager(assetsPath string) portainer.StackManager {
|
||||
return exec.NewStackManager(assetsPath)
|
||||
func initStackManager(assetsPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.StackManager, error) {
|
||||
return exec.NewStackManager(assetsPath, signatureService, fileService)
|
||||
}
|
||||
|
||||
func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
||||
|
@ -76,6 +77,10 @@ func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
|||
return nil
|
||||
}
|
||||
|
||||
func initDigitalSignatureService() portainer.DigitalSignatureService {
|
||||
return &crypto.ECDSAService{}
|
||||
}
|
||||
|
||||
func initCryptoService() portainer.CryptoService {
|
||||
return &crypto.Service{}
|
||||
}
|
||||
|
@ -173,6 +178,35 @@ func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService
|
|||
return &endpoints[0]
|
||||
}
|
||||
|
||||
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
private, public, err := fileService.LoadKeyPair()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return signatureService.ParseKeyPair(private, public)
|
||||
}
|
||||
|
||||
func generateAndStoreKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
private, public, err := signatureService.GenerateKeyPair()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateHeader, publicHeader := signatureService.PEMHeaders()
|
||||
return fileService.StoreKeyPair(private, public, privateHeader, publicHeader)
|
||||
}
|
||||
|
||||
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
existingKeyPair, err := fileService.KeyPairFilesExist()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if existingKeyPair {
|
||||
return loadAndParseKeyPair(fileService, signatureService)
|
||||
}
|
||||
return generateAndStoreKeyPair(fileService, signatureService)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flags := initCLI()
|
||||
|
||||
|
@ -181,19 +215,29 @@ func main() {
|
|||
store := initStore(*flags.Data)
|
||||
defer store.Close()
|
||||
|
||||
stackManager := initStackManager(*flags.Assets)
|
||||
|
||||
jwtService := initJWTService(!*flags.NoAuth)
|
||||
|
||||
cryptoService := initCryptoService()
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
ldapService := initLDAPService()
|
||||
|
||||
gitService := initGitService()
|
||||
|
||||
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
|
||||
|
||||
err := initSettings(store.SettingsService, flags)
|
||||
err := initKeyPair(fileService, digitalSignatureService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stackManager, err := initStackManager(*flags.Assets, digitalSignatureService, fileService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = initSettings(store.SettingsService, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -214,9 +258,10 @@ func main() {
|
|||
endpoint := &portainer.Endpoint{
|
||||
Name: "primary",
|
||||
URL: *flags.Endpoint,
|
||||
Type: portainer.DockerEnvironment,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: *flags.TLSVerify,
|
||||
TLSSkipVerify: false,
|
||||
TLSSkipVerify: *flags.TLSSkipVerify,
|
||||
TLSCACertPath: *flags.TLSCacert,
|
||||
TLSCertPath: *flags.TLSCert,
|
||||
TLSKeyPath: *flags.TLSKey,
|
||||
|
@ -225,6 +270,16 @@ func main() {
|
|||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
}
|
||||
|
||||
agentOnDockerEnvironment, err := client.ExecutePingOperationFromEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if agentOnDockerEnvironment {
|
||||
endpoint.Type = portainer.AgentOnDockerEnvironment
|
||||
}
|
||||
|
||||
err = store.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -292,6 +347,7 @@ func main() {
|
|||
FileService: fileService,
|
||||
LDAPService: ldapService,
|
||||
GitService: gitService,
|
||||
SignatureService: digitalSignatureService,
|
||||
SSL: *flags.SSL,
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const (
|
||||
// PrivateKeyPemHeader represents the header that is appended to the PEM file when
|
||||
// storing the private key.
|
||||
PrivateKeyPemHeader = "EC PRIVATE KEY"
|
||||
// PublicKeyPemHeader represents the header that is appended to the PEM file when
|
||||
// storing the public key.
|
||||
PublicKeyPemHeader = "ECDSA PUBLIC KEY"
|
||||
)
|
||||
|
||||
// ECDSAService is a service used to create digital signatures when communicating with
|
||||
// an agent based environment. It will automatically generates a key pair using ECDSA or
|
||||
// can also reuse an existing ECDSA key pair.
|
||||
type ECDSAService struct {
|
||||
privateKey *ecdsa.PrivateKey
|
||||
publicKey *ecdsa.PublicKey
|
||||
encodedPubKey string
|
||||
}
|
||||
|
||||
// EncodedPublicKey returns the encoded version of the public that can be used
|
||||
// to be shared with other services. It's the hexadecimal encoding of the public key
|
||||
// content.
|
||||
func (service *ECDSAService) EncodedPublicKey() string {
|
||||
return service.encodedPubKey
|
||||
}
|
||||
|
||||
// PEMHeaders returns the ECDSA PEM headers.
|
||||
func (service *ECDSAService) PEMHeaders() (string, string) {
|
||||
return PrivateKeyPemHeader, PublicKeyPemHeader
|
||||
}
|
||||
|
||||
// ParseKeyPair parses existing private/public key pair content and associate
|
||||
// the parsed keys to the service.
|
||||
func (service *ECDSAService) ParseKeyPair(private, public []byte) error {
|
||||
privateKey, err := x509.ParseECPrivateKey(private)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.privateKey = privateKey
|
||||
|
||||
encodedKey := hex.EncodeToString(public)
|
||||
service.encodedPubKey = encodedKey
|
||||
|
||||
publicKey, err := x509.ParsePKIXPublicKey(public)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.publicKey = publicKey.(*ecdsa.PublicKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateKeyPair will create a new key pair using ECDSA.
|
||||
func (service *ECDSAService) GenerateKeyPair() ([]byte, []byte, error) {
|
||||
pubkeyCurve := elliptic.P256()
|
||||
|
||||
privatekey, err := ecdsa.GenerateKey(pubkeyCurve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
service.privateKey = privatekey
|
||||
service.publicKey = &privatekey.PublicKey
|
||||
|
||||
private, err := x509.MarshalECPrivateKey(service.privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
public, err := x509.MarshalPKIXPublicKey(service.publicKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
encodedKey := hex.EncodeToString(public)
|
||||
service.encodedPubKey = encodedKey
|
||||
|
||||
return private, public, nil
|
||||
}
|
||||
|
||||
// Sign creates a signature from a message.
|
||||
// It automatically hash the message using MD5 and creates a signature from
|
||||
// that hash.
|
||||
// It then encodes the generated signature in base64.
|
||||
func (service *ECDSAService) Sign(message string) (string, error) {
|
||||
digest := md5.New()
|
||||
digest.Write([]byte(message))
|
||||
hash := digest.Sum(nil)
|
||||
|
||||
r := big.NewInt(0)
|
||||
s := big.NewInt(0)
|
||||
|
||||
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
keyBytes := service.privateKey.Params().BitSize / 8
|
||||
|
||||
rBytes := r.Bytes()
|
||||
rBytesPadded := make([]byte, keyBytes)
|
||||
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
|
||||
|
||||
sBytes := s.Bytes()
|
||||
sBytesPadded := make([]byte, keyBytes)
|
||||
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
|
||||
|
||||
signature := append(rBytesPadded, sBytesPadded...)
|
||||
|
||||
return base64.RawStdEncoding.EncodeToString(signature), nil
|
||||
}
|
|
@ -33,7 +33,7 @@ func CreateTLSConfig(caCert, cert, key []byte, skipClientVerification, skipServe
|
|||
func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, error) {
|
||||
TLSConfig := &tls.Config{}
|
||||
|
||||
if config.TLSCertPath != "" && config.TLSKeyPath != "" {
|
||||
if config.TLS && config.TLSCertPath != "" && config.TLSKeyPath != "" {
|
||||
cert, err := tls.LoadX509KeyPair(config.TLSCertPath, config.TLSKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -42,7 +42,7 @@ func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, er
|
|||
TLSConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
if !config.TLSSkipVerify {
|
||||
if config.TLS && !config.TLSSkipVerify {
|
||||
caCert, err := ioutil.ReadFile(config.TLSCACertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -12,14 +12,34 @@ import (
|
|||
|
||||
// StackManager represents a service for managing stacks.
|
||||
type StackManager struct {
|
||||
binaryPath string
|
||||
binaryPath string
|
||||
signatureService portainer.DigitalSignatureService
|
||||
fileService portainer.FileService
|
||||
}
|
||||
|
||||
type dockerCLIConfiguration struct {
|
||||
HTTPHeaders struct {
|
||||
ManagerOperationHeader string `json:"X-PortainerAgent-ManagerOperation"`
|
||||
SignatureHeader string `json:"X-PortainerAgent-Signature"`
|
||||
PublicKey string `json:"X-PortainerAgent-PublicKey"`
|
||||
} `json:"HttpHeaders"`
|
||||
}
|
||||
|
||||
// NewStackManager initializes a new StackManager service.
|
||||
func NewStackManager(binaryPath string) *StackManager {
|
||||
return &StackManager{
|
||||
binaryPath: binaryPath,
|
||||
// It also updates the configuration of the Docker CLI binary.
|
||||
func NewStackManager(binaryPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*StackManager, error) {
|
||||
manager := &StackManager{
|
||||
binaryPath: binaryPath,
|
||||
signatureService: signatureService,
|
||||
fileService: fileService,
|
||||
}
|
||||
|
||||
err := manager.updateDockerCLIConfiguration(binaryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// Login executes the docker login command against a list of registries (including DockerHub).
|
||||
|
@ -99,9 +119,12 @@ func prepareDockerCommandAndArgs(binaryPath string, endpoint *portainer.Endpoint
|
|||
}
|
||||
|
||||
args := make([]string, 0)
|
||||
args = append(args, "--config", binaryPath)
|
||||
args = append(args, "-H", endpoint.URL)
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
if !endpoint.TLSConfig.TLS && endpoint.TLSConfig.TLSSkipVerify {
|
||||
args = append(args, "--tls")
|
||||
} else if endpoint.TLSConfig.TLS {
|
||||
args = append(args, "--tls")
|
||||
|
||||
if !endpoint.TLSConfig.TLSSkipVerify {
|
||||
|
@ -115,3 +138,22 @@ func prepareDockerCommandAndArgs(binaryPath string, endpoint *portainer.Endpoint
|
|||
|
||||
return command, args
|
||||
}
|
||||
|
||||
func (manager *StackManager) updateDockerCLIConfiguration(binaryPath string) error {
|
||||
config := dockerCLIConfiguration{}
|
||||
config.HTTPHeaders.ManagerOperationHeader = "1"
|
||||
|
||||
signature, err := manager.signatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.HTTPHeaders.SignatureHeader = signature
|
||||
config.HTTPHeaders.PublicKey = manager.signatureService.EncodedPublicKey()
|
||||
|
||||
err = manager.fileService.WriteJSONToFile(path.Join(binaryPath, "config.json"), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package filesystem
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
@ -26,6 +28,10 @@ const (
|
|||
ComposeStorePath = "compose"
|
||||
// ComposeFileDefaultName represents the default name of a compose file.
|
||||
ComposeFileDefaultName = "docker-compose.yml"
|
||||
// PrivateKeyFile represents the name on disk of the file containing the private key.
|
||||
PrivateKeyFile = "portainer.key"
|
||||
// PublicKeyFile represents the name on disk of the file containing the public key.
|
||||
PublicKeyFile = "portainer.pub"
|
||||
)
|
||||
|
||||
// Service represents a service for managing files and directories.
|
||||
|
@ -198,6 +204,69 @@ func (service *Service) GetFileContent(filePath string) (string, error) {
|
|||
return string(content), nil
|
||||
}
|
||||
|
||||
// WriteJSONToFile writes JSON to the specified file.
|
||||
func (service *Service) WriteJSONToFile(path string, content interface{}) error {
|
||||
jsonContent, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, jsonContent, 0644)
|
||||
}
|
||||
|
||||
// KeyPairFilesExist checks for the existence of the key files.
|
||||
func (service *Service) KeyPairFilesExist() (bool, error) {
|
||||
privateKeyPath := path.Join(service.dataStorePath, PrivateKeyFile)
|
||||
exists, err := fileExists(privateKeyPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
publicKeyPath := path.Join(service.dataStorePath, PublicKeyFile)
|
||||
exists, err = fileExists(publicKeyPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// StoreKeyPair store the specified keys content as PEM files on disk.
|
||||
func (service *Service) StoreKeyPair(private, public []byte, privatePEMHeader, publicPEMHeader string) error {
|
||||
err := service.createPEMFileInStore(private, privatePEMHeader, PrivateKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadKeyPair retrieve the content of both key files on disk.
|
||||
func (service *Service) LoadKeyPair() ([]byte, []byte, error) {
|
||||
privateKey, err := service.getContentFromPEMFile(PrivateKeyFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
publicKey, err := service.getContentFromPEMFile(PublicKeyFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return privateKey, publicKey, nil
|
||||
}
|
||||
|
||||
// createDirectoryInStore creates a new directory in the file store
|
||||
func (service *Service) createDirectoryInStore(name string) error {
|
||||
path := path.Join(service.fileStorePath, name)
|
||||
|
@ -221,3 +290,43 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) createPEMFileInStore(content []byte, fileType, filePath string) error {
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
block := &pem.Block{Type: fileType, Bytes: content}
|
||||
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
err = pem.Encode(out, block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
|
||||
fileContent, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(fileContent)
|
||||
return block.Bytes, nil
|
||||
}
|
||||
|
||||
func fileExists(filePath string) (bool, error) {
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
)
|
||||
|
||||
// ExecutePingOperationFromEndpoint will send a SystemPing operation HTTP request to a Docker environment
|
||||
// using the specified endpoint configuration. It is used exclusively when
|
||||
// specifying an endpoint from the CLI via the -H flag.
|
||||
func ExecutePingOperationFromEndpoint(endpoint *portainer.Endpoint) (bool, error) {
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
transport := &http.Transport{}
|
||||
|
||||
scheme := "http"
|
||||
|
||||
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
||||
tlsConfig, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
scheme = "https"
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 3,
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
target := strings.Replace(endpoint.URL, "tcp://", scheme+"://", 1)
|
||||
return pingOperation(client, target)
|
||||
}
|
||||
|
||||
// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment
|
||||
// using the specified host and optional TLS configuration.
|
||||
func ExecutePingOperation(host string, tlsConfig *tls.Config) (bool, error) {
|
||||
transport := &http.Transport{}
|
||||
|
||||
scheme := "http"
|
||||
if tlsConfig != nil {
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 3,
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
target := strings.Replace(host, "tcp://", scheme+"://", 1)
|
||||
return pingOperation(client, target)
|
||||
}
|
||||
|
||||
func pingOperation(client *http.Client, target string) (bool, error) {
|
||||
pingOperationURL := target + "/_ping"
|
||||
|
||||
response, err := client.Get(pingOperationURL)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
agentOnDockerEnvironment := false
|
||||
if response.Header.Get(portainer.PortainerAgentHeader) != "" {
|
||||
agentOnDockerEnvironment = true
|
||||
}
|
||||
|
||||
return agentOnDockerEnvironment, nil
|
||||
}
|
|
@ -2,12 +2,11 @@ package handler
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
"github.com/portainer/portainer/http/client"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
@ -63,10 +62,6 @@ func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManag
|
|||
}
|
||||
|
||||
type (
|
||||
postEndpointsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putEndpointAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"-"`
|
||||
AuthorizedTeams []int `valid:"-"`
|
||||
|
@ -125,44 +120,26 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
|
|||
encodeJSON(w, filteredEndpoints, handler.Logger)
|
||||
}
|
||||
|
||||
func sendPingRequest(host string, tlsConfig *tls.Config) error {
|
||||
transport := &http.Transport{}
|
||||
|
||||
scheme := "http"
|
||||
if tlsConfig != nil {
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 3,
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
pingOperationURL := strings.Replace(host, "tcp://", scheme+"://", 1) + "/_ping"
|
||||
_, err := client.Get(pingOperationURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
|
||||
|
||||
tlsConfig, err := crypto.CreateTLSConfig(payload.caCert, payload.cert, payload.key, payload.skipTLSClientVerification, payload.skipTLSServerVerification)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sendPingRequest(payload.url, tlsConfig)
|
||||
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.url, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpointType := portainer.DockerEnvironment
|
||||
if agentOnDockerEnvironment {
|
||||
endpointType = portainer.AgentOnDockerEnvironment
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: payload.name,
|
||||
URL: payload.url,
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.groupID),
|
||||
PublicURL: payload.publicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
|
@ -224,17 +201,22 @@ func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPa
|
|||
}
|
||||
|
||||
func (handler *EndpointHandler) createUnsecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
|
||||
endpointType := portainer.DockerEnvironment
|
||||
|
||||
if !strings.HasPrefix(payload.url, "unix://") {
|
||||
err := sendPingRequest(payload.url, nil)
|
||||
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agentOnDockerEnvironment {
|
||||
endpointType = portainer.AgentOnDockerEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: payload.name,
|
||||
URL: payload.url,
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.groupID),
|
||||
PublicURL: payload.publicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
|
@ -331,7 +313,7 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
|||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
|
||||
encodeJSON(w, &endpoint, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetEndpoint handles GET requests on /endpoints/:id
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
@ -16,119 +16,135 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/koding/websocketproxy"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
"golang.org/x/net/websocket"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
)
|
||||
|
||||
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
|
||||
type WebSocketHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
}
|
||||
type (
|
||||
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
|
||||
WebSocketHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
connectionUpgrader websocket.Upgrader
|
||||
}
|
||||
|
||||
webSocketExecRequestParams struct {
|
||||
execID string
|
||||
nodeName string
|
||||
endpoint *portainer.Endpoint
|
||||
}
|
||||
|
||||
execStartOperationPayload struct {
|
||||
Tty bool
|
||||
Detach bool
|
||||
}
|
||||
)
|
||||
|
||||
// NewWebSocketHandler returns a new instance of WebSocketHandler.
|
||||
func NewWebSocketHandler() *WebSocketHandler {
|
||||
h := &WebSocketHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
connectionUpgrader: websocket.Upgrader{},
|
||||
}
|
||||
h.Handle("/websocket/exec", websocket.Handler(h.webSocketDockerExec))
|
||||
h.HandleFunc("/websocket/exec", h.handleWebsocketExec).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||
qry := ws.Request().URL.Query()
|
||||
execID := qry.Get("id")
|
||||
edpID := qry.Get("endpointId")
|
||||
|
||||
parsedID, err := strconv.Atoi(edpID)
|
||||
if err != nil {
|
||||
log.Printf("Unable to parse endpoint ID: %s", err)
|
||||
// handleWebsocketExec handles GET requests on /websocket/exec?id=<execID>&endpointId=<endpointID>&nodeName=<nodeName>
|
||||
// If the nodeName query parameter is present, the request will be proxied to the underlying agent endpoint.
|
||||
// If the nodeName query parameter is not specified, the request will be upgraded to the websocket protocol and
|
||||
// an ExecStart operation HTTP request will be created and hijacked.
|
||||
func (handler *WebSocketHandler) handleWebsocketExec(w http.ResponseWriter, r *http.Request) {
|
||||
paramExecID := r.FormValue("id")
|
||||
paramEndpointID := r.FormValue("endpointId")
|
||||
if paramExecID == "" || paramEndpointID == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(parsedID)
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
endpointID, err := strconv.Atoi(paramEndpointID)
|
||||
if err != nil {
|
||||
log.Printf("Unable to retrieve endpoint: %s", err)
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
log.Printf("Unable to parse endpoint URL: %s", err)
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var host string
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
host = endpointURL.Host
|
||||
} else if endpointURL.Scheme == "unix" {
|
||||
host = endpointURL.Path
|
||||
params := &webSocketExecRequestParams{
|
||||
endpoint: endpoint,
|
||||
execID: paramExecID,
|
||||
nodeName: r.FormValue("nodeName"),
|
||||
}
|
||||
|
||||
// TODO: Should not be managed here
|
||||
var tlsConfig *tls.Config
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||
return
|
||||
err = handler.handleRequest(w, r, params)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *WebSocketHandler) handleRequest(w http.ResponseWriter, r *http.Request, params *webSocketExecRequestParams) error {
|
||||
if params.nodeName != "" {
|
||||
return handler.proxyWebsocketRequest(w, r, params)
|
||||
}
|
||||
|
||||
websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer websocketConn.Close()
|
||||
|
||||
return hijackExecStartOperation(websocketConn, params.endpoint, params.execID)
|
||||
}
|
||||
|
||||
func (handler *WebSocketHandler) proxyWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketExecRequestParams) error {
|
||||
agentURL, err := url.Parse(params.endpoint.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
agentURL.Scheme = "ws"
|
||||
proxy := websocketproxy.NewProxy(agentURL)
|
||||
|
||||
if params.endpoint.TLSConfig.TLS || params.endpoint.TLSConfig.TLSSkipVerify {
|
||||
agentURL.Scheme = "wss"
|
||||
proxy.Dialer = &websocket.Dialer{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: params.endpoint.TLSConfig.TLSSkipVerify,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if err := hijack(host, endpointURL.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
||||
log.Fatalf("error during hijack: %s", err)
|
||||
return
|
||||
signature, err := handler.SignatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy.Director = func(incoming *http.Request, out http.Header) {
|
||||
out.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
||||
}
|
||||
|
||||
r.Header.Del("Origin")
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type execConfig struct {
|
||||
Tty bool
|
||||
Detach bool
|
||||
}
|
||||
|
||||
// hijack allows to upgrade an HTTP connection to a TCP connection
|
||||
// It redirects IO streams for stdin, stdout and stderr to a websocket
|
||||
func hijack(addr, scheme, method, path string, tlsConfig *tls.Config, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, data interface{}) error {
|
||||
execConfig := &execConfig{
|
||||
Tty: true,
|
||||
Detach: false,
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(execConfig)
|
||||
func hijackExecStartOperation(websocketConn *websocket.Conn, endpoint *portainer.Endpoint, execID string) error {
|
||||
dial, err := createDial(endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling exec config: %s", err)
|
||||
}
|
||||
|
||||
rdr := bytes.NewReader(buf)
|
||||
|
||||
req, err := http.NewRequest(method, path, rdr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during hijack request: %s", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Docker-Client")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Upgrade", "tcp")
|
||||
req.Host = addr
|
||||
|
||||
var (
|
||||
dial net.Conn
|
||||
dialErr error
|
||||
)
|
||||
|
||||
if tlsConfig == nil {
|
||||
dial, dialErr = net.Dial(scheme, addr)
|
||||
} else {
|
||||
dial, dialErr = tls.Dial(scheme, addr, tlsConfig)
|
||||
}
|
||||
|
||||
if dialErr != nil {
|
||||
return dialErr
|
||||
return err
|
||||
}
|
||||
|
||||
// When we set up a TCP connection for hijack, there could be long periods
|
||||
|
@ -140,57 +156,128 @@ func hijack(addr, scheme, method, path string, tlsConfig *tls.Config, setRawTerm
|
|||
tcpConn.SetKeepAlive(true)
|
||||
tcpConn.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
|
||||
httpConn := httputil.NewClientConn(dial, nil)
|
||||
defer httpConn.Close()
|
||||
|
||||
execStartRequest, err := createExecStartRequest(execID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientconn := httputil.NewClientConn(dial, nil)
|
||||
defer clientconn.Close()
|
||||
|
||||
// Server hijacks the connection, error 'connection closed' expected
|
||||
clientconn.Do(req)
|
||||
|
||||
rwc, br := clientconn.Hijack()
|
||||
defer rwc.Close()
|
||||
|
||||
if started != nil {
|
||||
started <- rwc
|
||||
err = hijackRequest(websocketConn, httpConn, execStartRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var receiveStdout chan error
|
||||
|
||||
if stdout != nil || stderr != nil {
|
||||
go func() (err error) {
|
||||
if setRawTerminal && stdout != nil {
|
||||
_, err = io.Copy(stdout, br)
|
||||
}
|
||||
return err
|
||||
}()
|
||||
}
|
||||
|
||||
go func() error {
|
||||
if in != nil {
|
||||
io.Copy(rwc, in)
|
||||
}
|
||||
|
||||
if conn, ok := rwc.(interface {
|
||||
CloseWrite() error
|
||||
}); ok {
|
||||
if err := conn.CloseWrite(); err != nil {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
if stdout != nil || stderr != nil {
|
||||
if err := <-receiveStdout; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
fmt.Println(br)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDial(endpoint *portainer.Endpoint) (net.Conn, error) {
|
||||
url, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var host string
|
||||
if url.Scheme == "tcp" {
|
||||
host = url.Host
|
||||
} else if url.Scheme == "unix" {
|
||||
host = url.Path
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tls.Dial(url.Scheme, host, tlsConfig)
|
||||
}
|
||||
|
||||
return net.Dial(url.Scheme, host)
|
||||
}
|
||||
|
||||
func createExecStartRequest(execID string) (*http.Request, error) {
|
||||
execStartOperationPayload := &execStartOperationPayload{
|
||||
Tty: true,
|
||||
Detach: false,
|
||||
}
|
||||
|
||||
encodedBody := bytes.NewBuffer(nil)
|
||||
err := json.NewEncoder(encodedBody).Encode(execStartOperationPayload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", "/exec/"+execID+"/start", encodedBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Connection", "Upgrade")
|
||||
request.Header.Set("Upgrade", "tcp")
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func hijackRequest(websocketConn *websocket.Conn, httpConn *httputil.ClientConn, request *http.Request) error {
|
||||
// Server hijacks the connection, error 'connection closed' expected
|
||||
resp, err := httpConn.Do(request)
|
||||
if err != httputil.ErrPersistEOF {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
resp.Body.Close()
|
||||
return fmt.Errorf("unable to upgrade to tcp, received %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
tcpConn, brw := httpConn.Hijack()
|
||||
defer tcpConn.Close()
|
||||
|
||||
errorChan := make(chan error, 1)
|
||||
go streamFromTCPConnToWebsocketConn(websocketConn, brw, errorChan)
|
||||
go streamFromWebsocketConnToTCPConn(websocketConn, tcpConn, errorChan)
|
||||
|
||||
err = <-errorChan
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func streamFromWebsocketConnToTCPConn(websocketConn *websocket.Conn, tcpConn net.Conn, errorChan chan error) {
|
||||
for {
|
||||
_, in, err := websocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
break
|
||||
}
|
||||
|
||||
_, err = tcpConn.Write(in)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func streamFromTCPConnToWebsocketConn(websocketConn *websocket.Conn, br *bufio.Reader, errorChan chan error) {
|
||||
for {
|
||||
out := make([]byte, 1024)
|
||||
_, err := br.Read(out)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
break
|
||||
}
|
||||
|
||||
err = websocketConn.WriteMessage(websocket.TextMessage, out)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,9 +98,12 @@ func canUserAccessResource(userID portainer.UserID, userTeamIDs []portainer.Team
|
|||
}
|
||||
|
||||
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
|
||||
metadata := make(map[string]interface{})
|
||||
metadata["ResourceControl"] = resourceControl
|
||||
object["Portainer"] = metadata
|
||||
if object["Portainer"] == nil {
|
||||
object["Portainer"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
portainerMetadata := object["Portainer"].(map[string]interface{})
|
||||
portainerMetadata["ResourceControl"] = resourceControl
|
||||
return object
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ const (
|
|||
|
||||
// configListOperation extracts the response as a JSON object, loop through the configs array
|
||||
// decorate and/or filter the configs based on resource controls before rewriting the response
|
||||
func configListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func configListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
|
||||
// ConfigList response is a JSON array
|
||||
|
@ -39,7 +39,7 @@ func configListOperation(request *http.Request, response *http.Response, executo
|
|||
// configInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the config based on resource control (check are done based on the configID and optional Swarm service ID)
|
||||
// and either rewrite an access denied response or a decorated config.
|
||||
func configInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func configInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ConfigInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.30/#operation/ConfigInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
|
|
|
@ -16,7 +16,7 @@ const (
|
|||
|
||||
// containerListOperation extracts the response as a JSON object, loop through the containers array
|
||||
// decorate and/or filter the containers based on resource controls before rewriting the response
|
||||
func containerListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func containerListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// ContainerList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
|
@ -47,7 +47,7 @@ func containerListOperation(request *http.Request, response *http.Response, exec
|
|||
// containerInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the container based on resource control (check are done based on the containerID and optional Swarm service ID)
|
||||
// and either rewrite an access denied response or a decorated container.
|
||||
func containerInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func containerInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ContainerInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
|
|
|
@ -17,6 +17,7 @@ type proxyFactory struct {
|
|||
SettingsService portainer.SettingsService
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) newExtensionHTTPPRoxy(u *url.URL) http.Handler {
|
||||
|
@ -24,10 +25,11 @@ func (factory *proxyFactory) newExtensionHTTPPRoxy(u *url.URL) http.Handler {
|
|||
return newSingleHostReverseProxyWithHostHeader(u)
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool) (http.Handler, error) {
|
||||
u.Scheme = "https"
|
||||
proxy := factory.createDockerReverseProxy(u)
|
||||
config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||
|
||||
proxy := factory.createDockerReverseProxy(u, enableSignature)
|
||||
config, err := crypto.CreateTLSConfiguration(tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -36,14 +38,15 @@ func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, endpoint *portainer
|
|||
return proxy, nil
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL) http.Handler {
|
||||
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return factory.createDockerReverseProxy(u)
|
||||
return factory.createDockerReverseProxy(u, enableSignature)
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) newDockerSocketProxy(path string) http.Handler {
|
||||
proxy := &socketProxy{}
|
||||
transport := &proxyTransport{
|
||||
enableSignature: false,
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
|
@ -55,9 +58,10 @@ func (factory *proxyFactory) newDockerSocketProxy(path string) http.Handler {
|
|||
return proxy
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL) *httputil.ReverseProxy {
|
||||
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool) *httputil.ReverseProxy {
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
||||
transport := &proxyTransport{
|
||||
enableSignature: enableSignature,
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
|
@ -65,6 +69,11 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL) *httputil.Reve
|
|||
DockerHubService: factory.DockerHubService,
|
||||
dockerTransport: &http.Transport{},
|
||||
}
|
||||
|
||||
if enableSignature {
|
||||
transport.SignatureService = factory.SignatureService
|
||||
}
|
||||
|
||||
proxy.Transport = transport
|
||||
return proxy
|
||||
}
|
||||
|
|
|
@ -9,24 +9,37 @@ import (
|
|||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// Manager represents a service used to manage Docker proxies.
|
||||
type Manager struct {
|
||||
proxyFactory *proxyFactory
|
||||
proxies cmap.ConcurrentMap
|
||||
extensionProxies cmap.ConcurrentMap
|
||||
}
|
||||
type (
|
||||
// Manager represents a service used to manage Docker proxies.
|
||||
Manager struct {
|
||||
proxyFactory *proxyFactory
|
||||
proxies cmap.ConcurrentMap
|
||||
extensionProxies cmap.ConcurrentMap
|
||||
}
|
||||
|
||||
// ManagerParams represents the required parameters to create a new Manager instance.
|
||||
ManagerParams struct {
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
}
|
||||
)
|
||||
|
||||
// NewManager initializes a new proxy Service
|
||||
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService, registryService portainer.RegistryService, dockerHubService portainer.DockerHubService) *Manager {
|
||||
func NewManager(parameters *ManagerParams) *Manager {
|
||||
return &Manager{
|
||||
proxies: cmap.New(),
|
||||
extensionProxies: cmap.New(),
|
||||
proxyFactory: &proxyFactory{
|
||||
ResourceControlService: resourceControlService,
|
||||
TeamMembershipService: teamMembershipService,
|
||||
SettingsService: settingsService,
|
||||
RegistryService: registryService,
|
||||
DockerHubService: dockerHubService,
|
||||
ResourceControlService: parameters.ResourceControlService,
|
||||
TeamMembershipService: parameters.TeamMembershipService,
|
||||
SettingsService: parameters.SettingsService,
|
||||
RegistryService: parameters.RegistryService,
|
||||
DockerHubService: parameters.DockerHubService,
|
||||
SignatureService: parameters.SignatureService,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -41,14 +54,19 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
|
|||
return nil, err
|
||||
}
|
||||
|
||||
enableSignature := false
|
||||
if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
enableSignature = true
|
||||
}
|
||||
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLSConfig.TLS {
|
||||
proxy, err = manager.proxyFactory.newDockerHTTPSProxy(endpointURL, endpoint)
|
||||
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
||||
proxy, err = manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, enableSignature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
proxy = manager.proxyFactory.newDockerHTTPProxy(endpointURL)
|
||||
proxy = manager.proxyFactory.newDockerHTTPProxy(endpointURL, enableSignature)
|
||||
}
|
||||
} else {
|
||||
// Assume unix:// scheme
|
||||
|
|
|
@ -15,7 +15,7 @@ const (
|
|||
|
||||
// networkListOperation extracts the response as a JSON object, loop through the networks array
|
||||
// decorate and/or filter the networks based on resource controls before rewriting the response
|
||||
func networkListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func networkListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// NetworkList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
|
@ -39,7 +39,7 @@ func networkListOperation(request *http.Request, response *http.Response, execut
|
|||
// networkInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the network based on resource control and either rewrite an access denied response
|
||||
// or a decorated network.
|
||||
func networkInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func networkInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// NetworkInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
|
|
|
@ -14,7 +14,7 @@ const (
|
|||
|
||||
// secretListOperation extracts the response as a JSON object, loop through the secrets array
|
||||
// decorate and/or filter the secrets based on resource controls before rewriting the response
|
||||
func secretListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func secretListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
|
||||
// SecretList response is a JSON array
|
||||
|
@ -39,7 +39,7 @@ func secretListOperation(request *http.Request, response *http.Response, executo
|
|||
// secretInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the secret based on resource control (check are done based on the secretID and optional Swarm service ID)
|
||||
// and either rewrite an access denied response or a decorated secret.
|
||||
func secretInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func secretInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// SecretInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
|
|
|
@ -15,7 +15,7 @@ const (
|
|||
|
||||
// serviceListOperation extracts the response as a JSON array, loop through the service array
|
||||
// decorate and/or filter the services based on resource controls before rewriting the response
|
||||
func serviceListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func serviceListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// ServiceList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
|
@ -39,7 +39,7 @@ func serviceListOperation(request *http.Request, response *http.Response, execut
|
|||
// serviceInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the service based on resource control and either rewrite an access denied response
|
||||
// or a decorated service.
|
||||
func serviceInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func serviceInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ServiceInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
|
|
|
@ -15,7 +15,7 @@ const (
|
|||
|
||||
// taskListOperation extracts the response as a JSON object, loop through the tasks array
|
||||
// and filter the tasks based on resource controls before rewriting the response
|
||||
func taskListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func taskListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
|
||||
// TaskList response is a JSON array
|
||||
|
|
|
@ -17,11 +17,13 @@ var apiVersionRe = regexp.MustCompile(`(/v[0-9]\.[0-9]*)?`)
|
|||
type (
|
||||
proxyTransport struct {
|
||||
dockerTransport *http.Transport
|
||||
enableSignature bool
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SettingsService portainer.SettingsService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
}
|
||||
restrictedOperationContext struct {
|
||||
isAdmin bool
|
||||
|
@ -45,7 +47,7 @@ type (
|
|||
operationContext *restrictedOperationContext
|
||||
labelBlackList []portainer.Pair
|
||||
}
|
||||
restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
|
||||
restrictedOperationRequest func(*http.Response, *operationExecutor) error
|
||||
operationRequest func(*http.Request) error
|
||||
)
|
||||
|
||||
|
@ -61,6 +63,16 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
|
|||
path := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
||||
request.URL.Path = path
|
||||
|
||||
if p.enableSignature {
|
||||
signature, err := p.SignatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, p.SignatureService.EncodedPublicKey())
|
||||
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/configs"):
|
||||
return p.proxyConfigRequest(request)
|
||||
|
@ -392,7 +404,7 @@ func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request,
|
|||
return response, err
|
||||
}
|
||||
|
||||
err = operation(request, response, executor)
|
||||
err = operation(response, executor)
|
||||
return response, err
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ const (
|
|||
|
||||
// volumeListOperation extracts the response as a JSON object, loop through the volume array
|
||||
// decorate and/or filter the volumes based on resource controls before rewriting the response
|
||||
func volumeListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func volumeListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// VolumeList response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
|
@ -48,7 +48,7 @@ func volumeListOperation(request *http.Request, response *http.Response, executo
|
|||
// volumeInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the volume based on any existing resource control and either rewrite an access denied response
|
||||
// or a decorated volume.
|
||||
func volumeInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
func volumeInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// VolumeInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
|
|
|
@ -34,6 +34,7 @@ type Server struct {
|
|||
StackManager portainer.StackManager
|
||||
LDAPService portainer.LDAPService
|
||||
GitService portainer.GitService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
SSLCert string
|
||||
|
@ -43,7 +44,15 @@ type Server struct {
|
|||
// Start starts the HTTP server
|
||||
func (server *Server) Start() error {
|
||||
requestBouncer := security.NewRequestBouncer(server.JWTService, server.UserService, server.TeamMembershipService, server.AuthDisabled)
|
||||
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService, server.RegistryService, server.DockerHubService)
|
||||
proxyManagerParameters := &proxy.ManagerParams{
|
||||
ResourceControlService: server.ResourceControlService,
|
||||
TeamMembershipService: server.TeamMembershipService,
|
||||
SettingsService: server.SettingsService,
|
||||
RegistryService: server.RegistryService,
|
||||
DockerHubService: server.DockerHubService,
|
||||
SignatureService: server.SignatureService,
|
||||
}
|
||||
proxyManager := proxy.NewManager(proxyManagerParameters)
|
||||
|
||||
var fileHandler = handler.NewFileHandler(filepath.Join(server.AssetsPath, "public"))
|
||||
var authHandler = handler.NewAuthHandler(requestBouncer, server.AuthDisabled)
|
||||
|
@ -78,6 +87,7 @@ func (server *Server) Start() error {
|
|||
dockerHandler.ProxyManager = proxyManager
|
||||
var websocketHandler = handler.NewWebSocketHandler()
|
||||
websocketHandler.EndpointService = server.EndpointService
|
||||
websocketHandler.SignatureService = server.SignatureService
|
||||
var endpointHandler = handler.NewEndpointHandler(requestBouncer, server.EndpointManagement)
|
||||
endpointHandler.EndpointService = server.EndpointService
|
||||
endpointHandler.EndpointGroupService = server.EndpointGroupService
|
||||
|
|
|
@ -24,6 +24,7 @@ type (
|
|||
NoAnalytics *bool
|
||||
Templates *string
|
||||
TLSVerify *bool
|
||||
TLSSkipVerify *bool
|
||||
TLSCacert *string
|
||||
TLSCert *string
|
||||
TLSKey *string
|
||||
|
@ -168,11 +169,15 @@ type (
|
|||
// EndpointID represents an endpoint identifier.
|
||||
EndpointID int
|
||||
|
||||
// EndpointType represents the type of an endpoint.
|
||||
EndpointType int
|
||||
|
||||
// Endpoint represents a Docker endpoint with all the info required
|
||||
// to connect to it.
|
||||
Endpoint struct {
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Type EndpointType `json:"Type"`
|
||||
URL string `json:"URL"`
|
||||
GroupID EndpointGroupID `json:"GroupId"`
|
||||
PublicURL string `json:"PublicURL"`
|
||||
|
@ -378,6 +383,15 @@ type (
|
|||
CompareHashAndData(hash string, data string) error
|
||||
}
|
||||
|
||||
// DigitalSignatureService represents a service to manage digital signatures.
|
||||
DigitalSignatureService interface {
|
||||
ParseKeyPair(private, public []byte) error
|
||||
GenerateKeyPair() ([]byte, []byte, error)
|
||||
EncodedPublicKey() string
|
||||
PEMHeaders() (string, string)
|
||||
Sign(message string) (string, error)
|
||||
}
|
||||
|
||||
// JWTService represents a service for managing JWT tokens.
|
||||
JWTService interface {
|
||||
GenerateToken(data *TokenData) (string, error)
|
||||
|
@ -395,6 +409,10 @@ type (
|
|||
GetStackProjectPath(stackIdentifier string) string
|
||||
StoreStackFileFromString(stackIdentifier, fileName, stackFileContent string) (string, error)
|
||||
StoreStackFileFromReader(stackIdentifier, fileName string, r io.Reader) (string, error)
|
||||
KeyPairFilesExist() (bool, error)
|
||||
StoreKeyPair(private, public []byte, privatePEMHeader, publicPEMHeader string) error
|
||||
LoadKeyPair() ([]byte, []byte, error)
|
||||
WriteJSONToFile(path string, content interface{}) error
|
||||
}
|
||||
|
||||
// GitService represents a service for managing Git.
|
||||
|
@ -427,9 +445,20 @@ const (
|
|||
// APIVersion is the version number of the Portainer API.
|
||||
APIVersion = "1.16.5"
|
||||
// DBVersion is the version number of the Portainer database.
|
||||
DBVersion = 9
|
||||
DBVersion = 10
|
||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||
PortainerAgentHeader = "Portainer-Agent"
|
||||
// PortainerAgentTargetHeader represent the name of the header containing the target node name.
|
||||
PortainerAgentTargetHeader = "X-PortainerAgent-Target"
|
||||
// PortainerAgentSignatureHeader represent the name of the header containing the digital signature
|
||||
PortainerAgentSignatureHeader = "X-PortainerAgent-Signature"
|
||||
// PortainerAgentPublicKeyHeader represent the name of the header containing the public key
|
||||
PortainerAgentPublicKeyHeader = "X-PortainerAgent-PublicKey"
|
||||
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
||||
// to be used when communicating with an agent
|
||||
PortainerAgentSignatureMessage = "Portainer-App"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -494,3 +523,11 @@ const (
|
|||
// StoridgeEndpointExtension represents the Storidge extension
|
||||
StoridgeEndpointExtension
|
||||
)
|
||||
|
||||
const (
|
||||
_ EndpointType = iota
|
||||
// DockerEnvironment represents an endpoint connected to a Docker environment
|
||||
DockerEnvironment
|
||||
// AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment
|
||||
AgentOnDockerEnvironment
|
||||
)
|
||||
|
|
|
@ -238,7 +238,7 @@ paths:
|
|||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/EndpointCreateResponse"
|
||||
$ref: "#/definitions/Endpoint"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
|
|
|
@ -17,6 +17,7 @@ angular.module('portainer', [
|
|||
'luegg.directives',
|
||||
'portainer.templates',
|
||||
'portainer.app',
|
||||
'portainer.agent',
|
||||
'portainer.docker',
|
||||
'extension.storidge',
|
||||
'rzModule']);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
angular.module('portainer.agent', []);
|
|
@ -0,0 +1,7 @@
|
|||
angular.module('portainer.agent').component('nodeSelector', {
|
||||
templateUrl: 'app/agent/components/node-selector/nodeSelector.html',
|
||||
controller: 'NodeSelectorController',
|
||||
bindings: {
|
||||
model: '='
|
||||
}
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
<div class="form-group">
|
||||
<label for="target_node" class="col-sm-1 control-label text-left">Node</label>
|
||||
<div class="col-sm-11">
|
||||
<select class="form-control"
|
||||
ng-model="$ctrl.model" ng-options="agent.NodeName as agent.NodeName for agent in $ctrl.agents"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
angular.module('portainer.agent')
|
||||
.controller('NodeSelectorController', ['AgentService', 'Notifications', function (AgentService, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
this.$onInit = function() {
|
||||
AgentService.agents()
|
||||
.then(function success(data) {
|
||||
ctrl.agents = data;
|
||||
if (!ctrl.model) {
|
||||
ctrl.model = data[0].NodeName;
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to load agents');
|
||||
});
|
||||
};
|
||||
|
||||
}]);
|
|
@ -0,0 +1,5 @@
|
|||
function AgentViewModel(data) {
|
||||
this.IPAddress = data.IPAddress;
|
||||
this.NodeName = data.NodeName;
|
||||
this.NodeRole = data.NodeRole;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
angular.module('portainer.agent')
|
||||
.factory('Agent', ['$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,24 @@
|
|||
angular.module('portainer.agent')
|
||||
.factory('AgentService', ['$q', 'Agent', function AgentServiceFactory($q, Agent) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.agents = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Agent.query({}).$promise
|
||||
.then(function success(data) {
|
||||
var agents = data.map(function (item) {
|
||||
return new AgentViewModel(item);
|
||||
});
|
||||
deferred.resolve(agents);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve agents', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -1,5 +1,6 @@
|
|||
angular.module('portainer')
|
||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'cfpLoadingBar', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, cfpLoadingBar) {
|
||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
||||
function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
||||
'use strict';
|
||||
|
||||
EndpointProvider.initialize();
|
||||
|
@ -27,6 +28,10 @@ angular.module('portainer')
|
|||
originalSet.apply(cfpLoadingBar, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
$transitions.onBefore({ to: 'docker.**' }, function() {
|
||||
HttpRequestHelper.resetAgentTargetQueue();
|
||||
});
|
||||
}]);
|
||||
|
||||
|
||||
|
|
|
@ -24,6 +24,17 @@ angular.module('portainer')
|
|||
$httpProvider.defaults.headers.put['Content-Type'] = 'application/json';
|
||||
$httpProvider.defaults.headers.patch['Content-Type'] = 'application/json';
|
||||
|
||||
$httpProvider.interceptors.push(['HttpRequestHelper', function(HttpRequestHelper) {
|
||||
return {
|
||||
request: function(config) {
|
||||
if (config.url.indexOf('/docker/') > -1) {
|
||||
config.headers['X-PortainerAgent-Target'] = HttpRequestHelper.portainerAgentTargetHeader();
|
||||
}
|
||||
return config;
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
AnalyticsProvider.setAccount('@@CONFIG_GA_ID');
|
||||
AnalyticsProvider.startOffline(true);
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
|
||||
var container = {
|
||||
name: 'docker.containers.container',
|
||||
url: '/:id',
|
||||
url: '/:id?nodeName',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/docker/views/containers/edit/container.html',
|
||||
|
@ -79,15 +79,12 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
|
||||
var containerCreation = {
|
||||
name: 'docker.containers.new',
|
||||
url: '/new',
|
||||
url: '/new?nodeName&from',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/docker/views/containers/create/createcontainer.html',
|
||||
controller: 'CreateContainerController'
|
||||
}
|
||||
},
|
||||
params: {
|
||||
from: ''
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -170,7 +167,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
|
||||
var image = {
|
||||
name: 'docker.images.image',
|
||||
url: '/:id',
|
||||
url: '/:id?nodeName',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/docker/views/images/edit/image.html',
|
||||
|
@ -203,7 +200,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
|
||||
var network = {
|
||||
name: 'docker.networks.network',
|
||||
url: '/:id',
|
||||
url: '/:id?nodeName',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/docker/views/networks/edit/network.html',
|
||||
|
@ -443,7 +440,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
|
||||
var volume = {
|
||||
name: 'docker.volumes.volume',
|
||||
url: '/:id',
|
||||
url: '/:id?nodeName',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/docker/views/volumes/edit/volume.html',
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
angular.module('portainer.docker').component('dashboardClusterAgentInfo', {
|
||||
templateUrl: 'app/docker/components/dashboard-cluster-agent-info/dashboardClusterAgentInfo.html',
|
||||
controller: 'DashboardClusterAgentInfoController'
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer-alt" title="Cluster information"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Nodes in the cluster</td>
|
||||
<td>{{ $ctrl.agentCount }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<a ui-sref="docker.swarm.visualizer"><i class="fa fa-object-group space-right" aria-hidden="true"></i>Go to cluster visualizer</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
|
@ -0,0 +1,16 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('DashboardClusterAgentInfoController', ['AgentService', 'Notifications',
|
||||
function (AgentService, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
this.$onInit = function() {
|
||||
AgentService.agents()
|
||||
.then(function success(data) {
|
||||
ctrl.agentCount = data.length;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve agent information');
|
||||
});
|
||||
};
|
||||
|
||||
}]);
|
|
@ -38,7 +38,7 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="(key, value) in $ctrl.dataset | itemsPerPage: $ctrl.state.paginatedItemLimit" ng-class="{active: item.Checked}">
|
||||
<td><a ui-sref="docker.networks.network({id: value.NetworkID})">{{ key }}</a></td>
|
||||
<td><a ui-sref="docker.networks.network({ id: value.NetworkID, nodeName: $ctrl.nodeName })">{{ key }}</a></td>
|
||||
<td>{{ value.IPAddress || '-' }}</td>
|
||||
<td>{{ value.Gateway || '-' }}</td>
|
||||
<td>{{ value.MacAddress || '-' }}</td>
|
||||
|
|
|
@ -11,6 +11,7 @@ angular.module('portainer.docker').component('containerNetworksDatatable', {
|
|||
joinNetworkAction: '<',
|
||||
joinNetworkActionInProgress: '<',
|
||||
leaveNetworkActionInProgress: '<',
|
||||
leaveNetworkAction: '<'
|
||||
leaveNetworkAction: '<',
|
||||
nodeName: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -156,6 +156,13 @@
|
|||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="$ctrl.showHostColumn">
|
||||
<a ng-click="$ctrl.changeOrderBy('NodeName')">
|
||||
Host
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Ports')">
|
||||
Published Ports
|
||||
|
@ -179,7 +186,7 @@
|
|||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="docker.containers.container({ id: item.Id })">{{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
|
||||
<a ui-sref="docker.containers.container({ id: item.Id, nodeName: item.NodeName })">{{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }}</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>
|
||||
|
@ -187,15 +194,16 @@
|
|||
</td>
|
||||
<td ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect">
|
||||
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
|
||||
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="docker.containers.container.stats({id: item.Id})" title="Stats"><i class="fa fa-chart-area space-right" aria-hidden="true"></i></a>
|
||||
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({id: item.Id})" title="Logs"><i class="fa fa-file-alt space-right" aria-hidden="true"></i></a>
|
||||
<a ng-if="$ctrl.settings.showQuickActionConsole" style="margin: 0 2.5px;" ui-sref="docker.containers.container.console({id: item.Id})" title="Console"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
|
||||
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="docker.containers.container.inspect({id: item.Id})" title="Inspect"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
|
||||
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="docker.containers.container.stats({id: item.Id, nodeName: item.NodeName})" title="Stats"><i class="fa fa-chart-area space-right" aria-hidden="true"></i></a>
|
||||
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({id: item.Id, nodeName: item.NodeName})" title="Logs"><i class="fa fa-file-alt space-right" aria-hidden="true"></i></a>
|
||||
<a ng-if="$ctrl.settings.showQuickActionConsole" style="margin: 0 2.5px;" ui-sref="docker.containers.container.console({id: item.Id, nodeName: item.NodeName})" title="Console"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
|
||||
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="docker.containers.container.inspect({id: item.Id, nodeName: item.NodeName})" title="Inspect"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||
<td><a ui-sref="docker.images.image({ id: item.Image })">{{ item.Image | trimshasum }}</a></td>
|
||||
<td>{{ item.IP ? item.IP : '-' }}</td>
|
||||
<td ng-if="$ctrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
|
||||
<td>
|
||||
<a ng-if="item.Ports.length > 0" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.publicUrl || p.host }}:{{p.public}}" target="_blank">
|
||||
<i class="fa fa-external-link-alt" aria-hidden="true"></i> {{ p.public }}:{{ p.private }}
|
||||
|
@ -210,10 +218,10 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="8" class="text-center text-muted">Loading...</td>
|
||||
<td colspan="9" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="8" class="text-center text-muted">No container available.</td>
|
||||
<td colspan="9" class="text-center text-muted">No container available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -10,6 +10,7 @@ angular.module('portainer.docker').component('containersDatatable', {
|
|||
reverseOrder: '<',
|
||||
showTextFilter: '<',
|
||||
showOwnershipColumn: '<',
|
||||
showHostColumn: '<',
|
||||
publicUrl: '<',
|
||||
containerNameTruncateSize: '<',
|
||||
startAction: '<',
|
||||
|
|
|
@ -93,6 +93,13 @@
|
|||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="$ctrl.showHostColumn">
|
||||
<a ng-click="$ctrl.changeOrderBy('NodeName')">
|
||||
Host
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -102,7 +109,7 @@
|
|||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="docker.images.image({id: item.Id})" class="monospaced">{{ item.Id | truncate:20 }}</a>
|
||||
<a ui-sref="docker.images.image({ id: item.Id, nodeName: item.NodeName })" class="monospaced">{{ item.Id | truncate:20 }}</a>
|
||||
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="::item.ContainerCount === 0">Unused</span>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -110,12 +117,13 @@
|
|||
</td>
|
||||
<td>{{ item.VirtualSize | humansize }}</td>
|
||||
<td>{{ item.Created | getisodatefromtimestamp }}</td>
|
||||
<td ng-if="$ctrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="4" class="text-center text-muted">No image available.</td>
|
||||
<td colspan="5" class="text-center text-muted">No image available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -9,6 +9,7 @@ angular.module('portainer.docker').component('imagesDatatable', {
|
|||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
showTextFilter: '<',
|
||||
showHostColumn: '<',
|
||||
removeAction: '<',
|
||||
forceRemoveAction: '<'
|
||||
}
|
||||
|
|
|
@ -81,6 +81,13 @@
|
|||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IPAM.Config[0].Gateway' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="$ctrl.showHostColumn">
|
||||
<a ng-click="$ctrl.changeOrderBy('NodeName')">
|
||||
Host
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="$ctrl.showOwnershipColumn">
|
||||
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
|
||||
Ownership
|
||||
|
@ -97,7 +104,7 @@
|
|||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="docker.networks.network({id: item.Id})" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
||||
<a ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
||||
</td>
|
||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||
<td>{{ item.Scope }}</td>
|
||||
|
@ -105,6 +112,7 @@
|
|||
<td>{{ item.IPAM.Driver }}</td>
|
||||
<td>{{ item.IPAM.Config[0].Subnet ? item.IPAM.Config[0].Subnet : '-' }}</td>
|
||||
<td>{{ item.IPAM.Config[0].Gateway ? item.IPAM.Config[0].Gateway : '-' }}</td>
|
||||
<td ng-if="$ctrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
|
||||
<td ng-if="$ctrl.showOwnershipColumn">
|
||||
<span>
|
||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||
|
@ -113,10 +121,10 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="8" class="text-center text-muted">Loading...</td>
|
||||
<td colspan="9" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="8" class="text-center text-muted">No network available.</td>
|
||||
<td colspan="9" class="text-center text-muted">No network available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -10,6 +10,7 @@ angular.module('portainer.docker').component('networksDatatable', {
|
|||
reverseOrder: '<',
|
||||
showTextFilter: '<',
|
||||
showOwnershipColumn: '<',
|
||||
showHostColumn: '<',
|
||||
removeAction: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -54,20 +54,26 @@
|
|||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="$ctrl.showLogsButton">Actions</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||
<td><a ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}</a></td>
|
||||
<td>
|
||||
<a ng-if="!$ctrl.agentProxy || !item.Container" ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}</a>
|
||||
<a ng-if="$ctrl.agentProxy && item.Container" ui-sref="docker.containers.container({ id: item.Container.Id, nodeName: item.Container.NodeName })" class="monospaced">{{ item.Id }}</a>
|
||||
</td>
|
||||
<td><span class="label label-{{ item.Status.State | taskstatusbadge }}">{{ item.Status.State }}</span></td>
|
||||
<td ng-if="$ctrl.showSlotColumn">{{ item.Slot ? item.Slot : '-' }}</td>
|
||||
<td>{{ item.NodeId | tasknodename: $ctrl.nodes }}</td>
|
||||
<td>{{ item.Updated | getisodate }}</td>
|
||||
<td ng-if="$ctrl.showLogsButton">
|
||||
<a ui-sref="docker.tasks.task.logs({id: item.Id})">
|
||||
<td>
|
||||
<a ui-sref="docker.tasks.task.logs({id: item.Id})" ng-if="$ctrl.showLogsButton" class="space-right">
|
||||
<i class="fa fa-file-alt" aria-hidden="true"></i> View logs
|
||||
</a>
|
||||
<a ui-sref="docker.containers.container.console({ id: item.Container.Id, nodeName: item.Container.NodeName })" ng-if="$ctrl.agentProxy">
|
||||
<i class="fa fa-terminal" aria-hidden="true"></i> Console
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
|
|
|
@ -11,6 +11,7 @@ angular.module('portainer.docker').component('tasksDatatable', {
|
|||
nodes: '<',
|
||||
showTextFilter: '<',
|
||||
showSlotColumn: '<',
|
||||
showLogsButton: '<'
|
||||
showLogsButton: '<',
|
||||
agentProxy: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -84,6 +84,13 @@
|
|||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Mountpoint' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="$ctrl.showHostColumn">
|
||||
<a ng-click="$ctrl.changeOrderBy('NodeName')">
|
||||
Host
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="$ctrl.showOwnershipColumn">
|
||||
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
|
||||
Ownership
|
||||
|
@ -100,12 +107,13 @@
|
|||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="docker.volumes.volume({id: item.Id})" class="monospaced">{{ item.Id | truncate:25 }}</a>
|
||||
<a ui-sref="docker.volumes.volume({ id: item.Id, nodeName: item.NodeName })" class="monospaced">{{ item.Id | truncate:25 }}</a>
|
||||
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="item.dangling">Unused</span>
|
||||
</td>
|
||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||
<td>{{ item.Driver }}</td>
|
||||
<td>{{ item.Mountpoint | truncatelr }}</td>
|
||||
<td ng-if="$ctrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
|
||||
<td ng-if="$ctrl.showOwnershipColumn">
|
||||
<span>
|
||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||
|
@ -114,10 +122,10 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
<td colspan="6" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="5" class="text-center text-muted">No volume available.</td>
|
||||
<td colspan="6" class="text-center text-muted">No volume available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -10,6 +10,7 @@ angular.module('portainer.docker').component('volumesDatatable', {
|
|||
reverseOrder: '<',
|
||||
showTextFilter: '<',
|
||||
showOwnershipColumn: '<',
|
||||
showHostColumn: '<',
|
||||
removeAction: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,12 +4,20 @@ angular.module('portainer.docker')
|
|||
|
||||
var helper = {};
|
||||
|
||||
helper.determineEndpointMode = function(info) {
|
||||
helper.determineEndpointMode = function(info, type) {
|
||||
var mode = {
|
||||
provider: '',
|
||||
role: ''
|
||||
role: '',
|
||||
agentProxy: false
|
||||
};
|
||||
|
||||
if (type === 2) {
|
||||
mode.provider = 'DOCKER_SWARM_MODE';
|
||||
mode.role = 'MANAGER';
|
||||
mode.agentProxy = true;
|
||||
return mode;
|
||||
}
|
||||
|
||||
if (!info.Swarm || _.isEmpty(info.Swarm.NodeID)) {
|
||||
if (info.ID === 'vSphere Integrated Containers') {
|
||||
mode.provider = 'VMWARE_VIC';
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
angular.module('portainer.docker')
|
||||
.factory('TaskHelper', [function TaskHelperFactory() {
|
||||
'use strict';
|
||||
|
||||
var helper = {};
|
||||
|
||||
helper.associateContainerToTask = function(task, containers) {
|
||||
for (var i = 0; i < containers.length; i++) {
|
||||
var container = containers[i];
|
||||
if (task.ContainerId === container.Id) {
|
||||
task.Container = container;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return helper;
|
||||
}]);
|
|
@ -34,6 +34,9 @@ function ContainerViewModel(data) {
|
|||
if (data.Portainer.ResourceControl) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||
}
|
||||
if (data.Portainer.Agent && data.Portainer.Agent.NodeName) {
|
||||
this.NodeName = data.Portainer.Agent.NodeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,10 @@ function ImageViewModel(data) {
|
|||
this.RepoTags = data.RepoTags;
|
||||
this.VirtualSize = data.VirtualSize;
|
||||
this.ContainerCount = data.ContainerCount;
|
||||
|
||||
if (data.Portainer && data.Portainer.Agent && data.Portainer.Agent.NodeName) {
|
||||
this.NodeName = data.Portainer.Agent.NodeName;
|
||||
}
|
||||
}
|
||||
|
||||
function ImageBuildModel(data) {
|
||||
|
|
|
@ -19,5 +19,8 @@ function NetworkViewModel(data) {
|
|||
if (data.Portainer.ResourceControl) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||
}
|
||||
if (data.Portainer.Agent && data.Portainer.Agent.NodeName) {
|
||||
this.NodeName = data.Portainer.Agent.NodeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,7 @@ function TaskViewModel(data) {
|
|||
this.Status = data.Status;
|
||||
this.ServiceId = data.ServiceID;
|
||||
this.NodeId = data.NodeID;
|
||||
if (data.Status && data.Status.ContainerStatus && data.Status.ContainerStatus.ContainerID) {
|
||||
this.ContainerId = data.Status.ContainerStatus.ContainerID;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,5 +14,8 @@ function VolumeViewModel(data) {
|
|||
if (data.Portainer.ResourceControl) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||
}
|
||||
if (data.Portainer.Agent && data.Portainer.Agent.NodeName) {
|
||||
this.NodeName = data.Portainer.Agent.NodeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,34 @@
|
|||
angular.module('portainer.docker')
|
||||
.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
|
||||
function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/containers/:id/:action', {
|
||||
name: '@name',
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
||||
query: {method: 'GET', params: {all: 0, action: 'json', filters: '@filters' }, isArray: true},
|
||||
get: {method: 'GET', params: {action: 'json'}},
|
||||
stop: {method: 'POST', params: {id: '@id', t: 5, action: 'stop'}},
|
||||
restart: {method: 'POST', params: {id: '@id', t: 5, action: 'restart'}},
|
||||
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
||||
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
||||
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
|
||||
query: {
|
||||
method: 'GET', params: { all: 0, action: 'json', filters: '@filters' },
|
||||
isArray: true
|
||||
},
|
||||
get: {
|
||||
method: 'GET', params: { action: 'json' }
|
||||
},
|
||||
stop: {
|
||||
method: 'POST', params: { id: '@id', t: 5, action: 'stop' }
|
||||
},
|
||||
restart: {
|
||||
method: 'POST', params: { id: '@id', t: 5, action: 'restart' }
|
||||
},
|
||||
kill: {
|
||||
method: 'POST', params: { id: '@id', action: 'kill' }
|
||||
},
|
||||
pause: {
|
||||
method: 'POST', params: { id: '@id', action: 'pause' }
|
||||
},
|
||||
unpause: {
|
||||
method: 'POST', params: { id: '@id', action: 'unpause' }
|
||||
},
|
||||
logs: {
|
||||
method: 'GET', params: { id: '@id', action: 'logs' },
|
||||
timeout: 4500, ignoreLoadingBar: true,
|
||||
|
@ -40,7 +56,7 @@ angular.module('portainer.docker')
|
|||
transformResponse: genericHandler
|
||||
},
|
||||
rename: {
|
||||
method: 'POST', params: {id: '@id', action: 'rename', name: '@name'},
|
||||
method: 'POST', params: { id: '@id', action: 'rename', name: '@name' },
|
||||
transformResponse: genericHandler
|
||||
},
|
||||
exec: {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.factory('Exec', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ExecFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
.factory('Exec', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
|
||||
function ExecFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/exec/:id/:action', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.factory('Image', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper', function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) {
|
||||
.factory('Image', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper',
|
||||
function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) {
|
||||
'use strict';
|
||||
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/images/:id/:action', {
|
||||
|
|
|
@ -1,16 +1,30 @@
|
|||
angular.module('portainer.docker')
|
||||
.factory('Network', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
.factory('Network', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
|
||||
function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/networks/:id/:action', {
|
||||
id: '@id',
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
||||
query: {method: 'GET', isArray: true},
|
||||
get: {method: 'GET'},
|
||||
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler, ignoreLoadingBar: true},
|
||||
remove: { method: 'DELETE', transformResponse: genericHandler },
|
||||
connect: {method: 'POST', params: {action: 'connect'}},
|
||||
disconnect: {method: 'POST', params: {action: 'disconnect'}}
|
||||
query: {
|
||||
method: 'GET', isArray: true
|
||||
},
|
||||
get: {
|
||||
method: 'GET'
|
||||
},
|
||||
create: {
|
||||
method: 'POST', params: {action: 'create'},
|
||||
transformResponse: genericHandler, ignoreLoadingBar: true
|
||||
},
|
||||
remove: {
|
||||
method: 'DELETE', transformResponse: genericHandler
|
||||
},
|
||||
connect: {
|
||||
method: 'POST', params: { action: 'connect' }
|
||||
},
|
||||
disconnect: {
|
||||
method: 'POST', params: { action: 'disconnect' }
|
||||
}
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.factory('Service', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper' ,function ServiceFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) {
|
||||
.factory('Service', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper',
|
||||
function ServiceFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/services/:id/:action', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
|
|
|
@ -6,7 +6,10 @@ angular.module('portainer.docker')
|
|||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
||||
info: { method: 'GET', params: { action: 'info' }, ignoreLoadingBar: true },
|
||||
info: {
|
||||
method: 'GET', params: { action: 'info' },
|
||||
ignoreLoadingBar: true
|
||||
},
|
||||
version: { method: 'GET', params: { action: 'version' }, ignoreLoadingBar: true, timeout: 4500 },
|
||||
events: {
|
||||
method: 'GET', params: { action: 'events', since: '@since', until: '@until' },
|
||||
|
|
|
@ -35,6 +35,34 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.startContainer = function(id) {
|
||||
return Container.start({ id: id }, {}).$promise;
|
||||
};
|
||||
|
||||
service.stopContainer = function(id) {
|
||||
return Container.stop({ id: id }, {}).$promise;
|
||||
};
|
||||
|
||||
service.restartContainer = function(id) {
|
||||
return Container.restart({ id: id }, {}).$promise;
|
||||
};
|
||||
|
||||
service.killContainer = function(id) {
|
||||
return Container.kill({ id: id }, {}).$promise;
|
||||
};
|
||||
|
||||
service.pauseContainer = function(id) {
|
||||
return Container.pause({ id: id }, {}).$promise;
|
||||
};
|
||||
|
||||
service.resumeContainer = function(id) {
|
||||
return Container.unpause({ id: id }, {}).$promise;
|
||||
};
|
||||
|
||||
service.renameContainer = function(id, newContainerName) {
|
||||
return Container.rename({id: id, name: newContainerName }, {}).$promise;
|
||||
};
|
||||
|
||||
service.createContainer = function(configuration) {
|
||||
var deferred = $q.defer();
|
||||
Container.create(configuration).$promise
|
||||
|
@ -51,30 +79,6 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.startContainer = function(containerID) {
|
||||
return Container.start({ id: containerID }, {}).$promise;
|
||||
};
|
||||
|
||||
service.stopContainer = function(containerID) {
|
||||
return Container.stop({ id: containerID }, {}).$promise;
|
||||
};
|
||||
|
||||
service.restartContainer = function(containerID) {
|
||||
return Container.restart({ id: containerID }, {}).$promise;
|
||||
};
|
||||
|
||||
service.killContainer = function(containerID) {
|
||||
return Container.kill({ id: containerID }, {}).$promise;
|
||||
};
|
||||
|
||||
service.pauseContainer = function(containerID) {
|
||||
return Container.pause({ id: containerID }, {}).$promise;
|
||||
};
|
||||
|
||||
service.resumeContainer = function(containerID) {
|
||||
return Container.unpause({ id: containerID }, {}).$promise;
|
||||
};
|
||||
|
||||
service.createAndStartContainer = function(configuration) {
|
||||
var deferred = $q.defer();
|
||||
var containerID;
|
||||
|
@ -95,7 +99,7 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe
|
|||
service.remove = function(container, removeVolumes) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Container.remove({id: container.Id, v: (removeVolumes) ? 1 : 0, force: true}).$promise
|
||||
Container.remove({ id: container.Id, v: (removeVolumes) ? 1 : 0, force: true }).$promise
|
||||
.then(function success(data) {
|
||||
if (data.message) {
|
||||
deferred.reject({ msg: data.message, err: data.message });
|
||||
|
@ -117,7 +121,7 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe
|
|||
service.createExec = function(execConfig) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Container.exec(execConfig).$promise
|
||||
Container.exec({}, execConfig).$promise
|
||||
.then(function success(data) {
|
||||
if (data.message) {
|
||||
deferred.reject({ msg: data.message, err: data.message });
|
||||
|
@ -158,7 +162,7 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe
|
|||
service.containerStats = function(id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Container.stats({id: id}).$promise
|
||||
Container.stats({ id: id }).$promise
|
||||
.then(function success(data) {
|
||||
var containerStats = new ContainerStatsViewModel(data);
|
||||
deferred.resolve(containerStats);
|
||||
|
@ -171,11 +175,11 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe
|
|||
};
|
||||
|
||||
service.containerTop = function(id) {
|
||||
return Container.top({id: id}).$promise;
|
||||
return Container.top({ id: id }).$promise;
|
||||
};
|
||||
|
||||
service.inspect = function(id) {
|
||||
return Container.inspect({id: id}).$promise;
|
||||
return Container.inspect({ id: id }).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
|
|
|
@ -7,7 +7,7 @@ angular.module('portainer.docker')
|
|||
var deferred = $q.defer();
|
||||
|
||||
$timeout(function() {
|
||||
Exec.resize({id: execId, height: height, width: width}).$promise
|
||||
Exec.resize({}, { id: execId, height: height, width: width }).$promise
|
||||
.then(function success(data) {
|
||||
if (data.message) {
|
||||
deferred.reject({ msg: 'Unable to exec into container', err: data.message });
|
||||
|
|
|
@ -19,7 +19,7 @@ angular.module('portainer.docker')
|
|||
service.network = function(id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Network.get({id: id}).$promise
|
||||
Network.get({ id: id }).$promise
|
||||
.then(function success(data) {
|
||||
var network = new NetworkViewModel(data);
|
||||
deferred.resolve(network);
|
||||
|
@ -65,5 +65,13 @@ angular.module('portainer.docker')
|
|||
return Network.remove({ id: id }).$promise;
|
||||
};
|
||||
|
||||
service.disconnectContainer = function(networkId, containerId, force) {
|
||||
return Network.disconnect({ id: networkId }, { Container: containerId, Force: force }).$promise;
|
||||
};
|
||||
|
||||
service.connectContainer = function(networkId, containerId) {
|
||||
return Network.connect({ id: networkId }, { Container: containerId }).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ContainerConsoleController', ['$scope', '$transition$', 'Container', 'Image', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ContainerService', 'ExecService',
|
||||
function ($scope, $transition$, Container, Image, EndpointProvider, Notifications, ContainerHelper, ContainerService, ExecService) {
|
||||
.controller('ContainerConsoleController', ['$scope', '$transition$', 'ContainerService', 'ImageService', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ExecService', 'HttpRequestHelper',
|
||||
function ($scope, $transition$, ContainerService, ImageService, EndpointProvider, Notifications, ContainerHelper, ExecService, HttpRequestHelper) {
|
||||
var socket, term;
|
||||
|
||||
$scope.state = {
|
||||
loaded: false,
|
||||
connected: false
|
||||
|
@ -8,8 +10,6 @@ function ($scope, $transition$, Container, Image, EndpointProvider, Notification
|
|||
|
||||
$scope.formValues = {};
|
||||
|
||||
var socket, term;
|
||||
|
||||
// Ensure the socket is closed before leaving the view
|
||||
$scope.$on('$stateChangeStart', function (event, next, current) {
|
||||
if (socket && socket !== null) {
|
||||
|
@ -17,23 +17,6 @@ function ($scope, $transition$, Container, Image, EndpointProvider, Notification
|
|||
}
|
||||
});
|
||||
|
||||
Container.get({id: $transition$.params().id}, function(d) {
|
||||
$scope.container = d;
|
||||
if (d.message) {
|
||||
Notifications.error('Error', d, 'Unable to retrieve container details');
|
||||
} else {
|
||||
Image.get({id: d.Image}, function(imgData) {
|
||||
$scope.imageOS = imgData.Os;
|
||||
$scope.formValues.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
|
||||
$scope.state.loaded = true;
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve image details');
|
||||
});
|
||||
}
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve container details');
|
||||
});
|
||||
|
||||
$scope.connect = function() {
|
||||
var termWidth = Math.floor(($('#terminal-container').width() - 20) / 8.39);
|
||||
var termHeight = 30;
|
||||
|
@ -54,6 +37,9 @@ function ($scope, $transition$, Container, Image, EndpointProvider, Notification
|
|||
.then(function success(data) {
|
||||
execId = data.Id;
|
||||
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID();
|
||||
if ($transition$.params().nodeName) {
|
||||
url += '&nodeName=' + $transition$.params().nodeName;
|
||||
}
|
||||
if (url.indexOf('https') > -1) {
|
||||
url = url.replace('https://', 'wss://');
|
||||
} else {
|
||||
|
@ -108,4 +94,25 @@ function ($scope, $transition$, Container, Image, EndpointProvider, Notification
|
|||
};
|
||||
};
|
||||
}
|
||||
|
||||
function initView() {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
|
||||
ContainerService.container($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
var container = data;
|
||||
$scope.container = container;
|
||||
return ImageService.image(container.Image);
|
||||
})
|
||||
.then(function success(data) {
|
||||
var image = data;
|
||||
$scope.imageOS = image.Os;
|
||||
$scope.formValues.command = image.Os === 'windows' ? 'powershell' : 'bash';
|
||||
$scope.state.loaded = true;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Error', err, 'Unable to retrieve container details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
dataset="containers" table-key="containers"
|
||||
order-by="Status" show-text-filter="true"
|
||||
show-ownership-column="applicationState.application.authentication"
|
||||
show-host-column="applicationState.endpoint.mode.agentProxy"
|
||||
public-url="state.publicURL"
|
||||
container-name-truncate-size="truncate_size"
|
||||
start-action="startAction"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ContainersController', ['$q', '$scope', '$state', '$filter', '$transition$', 'ContainerService', 'SystemService', 'Notifications', 'ModalService', 'EndpointProvider',
|
||||
function ($q, $scope, $state, $filter, $transition$, ContainerService, SystemService, Notifications, ModalService, EndpointProvider) {
|
||||
.controller('ContainersController', ['$q', '$scope', '$state', '$filter', '$transition$', 'ContainerService', 'SystemService', 'Notifications', 'ModalService', 'EndpointProvider', 'HttpRequestHelper',
|
||||
function ($q, $scope, $state, $filter, $transition$, ContainerService, SystemService, Notifications, ModalService, EndpointProvider, HttpRequestHelper) {
|
||||
$scope.state = {
|
||||
publicURL: EndpointProvider.endpointPublicURL()
|
||||
};
|
||||
|
@ -70,6 +70,7 @@ angular.module('portainer.docker')
|
|||
function executeActionOnContainerList(containers, action, successMessage, errorMessage) {
|
||||
var actionCount = containers.length;
|
||||
angular.forEach(containers, function (container) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName);
|
||||
action(container.Id)
|
||||
.then(function success() {
|
||||
Notifications.success(successMessage, container.Names[0]);
|
||||
|
@ -89,6 +90,7 @@ angular.module('portainer.docker')
|
|||
function removeAction(containers, cleanVolumes) {
|
||||
var actionCount = containers.length;
|
||||
angular.forEach(containers, function (container) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName);
|
||||
ContainerService.remove(container, cleanVolumes)
|
||||
.then(function success() {
|
||||
Notifications.success('Container successfully removed', container.Names[0]);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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',
|
||||
function ($q, $scope, $state, $timeout, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService, SystemService, SettingsService) {
|
||||
.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) {
|
||||
|
||||
$scope.formValues = {
|
||||
alwaysPull: true,
|
||||
|
@ -15,7 +15,8 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
AccessControlData: new AccessControlFormData(),
|
||||
CpuLimit: 0,
|
||||
MemoryLimit: 0,
|
||||
MemoryReservation: 0
|
||||
MemoryReservation: 0,
|
||||
NodeName: null
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
|
@ -388,12 +389,14 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
// Mac Address
|
||||
$scope.formValues.MacAddress = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].MacAddress;
|
||||
// ExtraHosts
|
||||
var extraHosts = $scope.config.HostConfig.ExtraHosts;
|
||||
for (var i = 0; i < extraHosts.length; i++) {
|
||||
var host = extraHosts[i];
|
||||
$scope.formValues.ExtraHosts.push({ 'value': host });
|
||||
if ($scope.config.HostConfig.ExtraHosts) {
|
||||
var extraHosts = $scope.config.HostConfig.ExtraHosts;
|
||||
for (var i = 0; i < extraHosts.length; i++) {
|
||||
var host = extraHosts[i];
|
||||
$scope.formValues.ExtraHosts.push({ 'value': host });
|
||||
}
|
||||
$scope.config.HostConfig.ExtraHosts = [];
|
||||
}
|
||||
$scope.config.HostConfig.ExtraHosts = [];
|
||||
}
|
||||
|
||||
function loadFromContainerEnvironmentVariables(d) {
|
||||
|
@ -491,6 +494,10 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
}
|
||||
|
||||
function initView() {
|
||||
var nodeName = $transition$.params().nodeName;
|
||||
$scope.formValues.NodeName = nodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
|
||||
Volume.query({}, function (d) {
|
||||
$scope.availableVolumes = d.Volumes;
|
||||
}, function (e) {
|
||||
|
@ -520,7 +527,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
Container.query({}, function (d) {
|
||||
var containers = d;
|
||||
$scope.runningContainers = containers;
|
||||
if ($transition$.params().from !== '') {
|
||||
if ($transition$.params().from) {
|
||||
loadFromContainerSpec();
|
||||
} else {
|
||||
$scope.fromContainer = {};
|
||||
|
@ -587,6 +594,8 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
|
||||
$scope.state.actionInProgress = true;
|
||||
var config = prepareConfiguration();
|
||||
var nodeName = $scope.formValues.NodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
createContainer(config, accessControlData);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -103,6 +103,16 @@
|
|||
<!-- !port-mapping-input-list -->
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<div ng-if="applicationState.endpoint.mode.agentProxy">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment
|
||||
</div>
|
||||
<!-- node-selection -->
|
||||
<node-selector
|
||||
model="formValues.NodeName">
|
||||
</node-selector>
|
||||
<!-- !node-selection -->
|
||||
</div>
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" resource-control="fromContainer.ResourceControl" ng-if="applicationState.application.authentication && fromContainer"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
|
|
|
@ -20,12 +20,12 @@
|
|||
<button class="btn btn-primary btn-sm" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||
<button class="btn btn-danger btn-sm" ng-click="confirmRemove()"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-disabled="state.recreateContainerInProgress" ng-click="recreate()" button-spinner="state.recreateContainerInProgress" ng-if="!container.Config.Labels['com.docker.swarm.service.id']">
|
||||
<div class="btn-group" role="group" aria-label="..." ng-if="!container.Config.Labels['com.docker.swarm.service.id']">
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-disabled="state.recreateContainerInProgress" ng-click="recreate()" button-spinner="state.recreateContainerInProgress">
|
||||
<span ng-hide="state.recreateContainerInProgress"><i class="fa fa-sync space-right" aria-hidden="true"></i>Recreate</span>
|
||||
<span ng-show="state.recreateContainerInProgress">Recreation in progress...</span>
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" ng-click="duplicate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-copy space-right" aria-hidden="true"></i>Duplicate/Edit</button>
|
||||
<a class="btn btn-primary btn-sm" type="button" ui-sref="docker.containers.new({ from: container.Id, nodeName: nodeName })"><i class="fa fa-copy space-right" aria-hidden="true"></i>Duplicate/Edit</a>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
@ -84,10 +84,10 @@
|
|||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<a class="btn" type="button" ui-sref="docker.containers.container.stats({id: container.Id})"><i class="fa fa-chart-area space-right" aria-hidden="true"></i>Stats</a>
|
||||
<a class="btn" type="button" ui-sref="docker.containers.container.logs({id: container.Id})"><i class="fa fa-file-alt space-right" aria-hidden="true"></i>Logs</a>
|
||||
<a class="btn" type="button" ui-sref="docker.containers.container.console({id: container.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
|
||||
<a class="btn" type="button" ui-sref="docker.containers.container.inspect({id: container.Id})"><i class="fa fa-info-circle space-right" aria-hidden="true"></i>Inspect</a>
|
||||
<a class="btn" type="button" ui-sref="docker.containers.container.stats({ id: container.Id })"><i class="fa fa-chart-area space-right" aria-hidden="true"></i>Stats</a>
|
||||
<a class="btn" type="button" ui-sref="docker.containers.container.logs({ id: container.Id })"><i class="fa fa-file-alt space-right" aria-hidden="true"></i>Logs</a>
|
||||
<a class="btn" type="button" ui-sref="docker.containers.container.console({ id: container.Id })"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
|
||||
<a class="btn" type="button" ui-sref="docker.containers.container.inspect({ id: container.Id })"><i class="fa fa-info-circle space-right" aria-hidden="true"></i>Inspect</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -184,7 +184,7 @@
|
|||
<tbody>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
<td><a ui-sref="docker.images.image({id: container.Image})">{{ container.Image }}</a></td>
|
||||
<td><a ui-sref="docker.images.image({ id: container.Image, nodeName: nodeName })">{{ container.Image }}</a></td>
|
||||
</tr>
|
||||
<tr ng-if="portBindings.length > 0">
|
||||
<td>Port configuration</td>
|
||||
|
@ -259,7 +259,7 @@
|
|||
<tbody>
|
||||
<tr ng-repeat="vol in container.Mounts">
|
||||
<td ng-if="vol.Type === 'bind'">{{ vol.Source }}</td>
|
||||
<td ng-if="vol.Type === 'volume'"><a ui-sref="docker.volumes.volume({id: vol.Name})">{{ vol.Name }}</a></td>
|
||||
<td ng-if="vol.Type === 'volume'"><a ui-sref="docker.volumes.volume({ id: vol.Name, nodeName: nodeName })">{{ vol.Name }}</a></td>
|
||||
<td>{{ vol.Destination }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -280,6 +280,7 @@
|
|||
join-network-action-in-progress="state.joinNetworkInProgress"
|
||||
leave-network-action="containerLeaveNetwork"
|
||||
leave-network-action-in-progress="state.leaveNetworkInProgress"
|
||||
node-name="nodeName"
|
||||
></container-networks-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Container', 'Commit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService',
|
||||
function ($q, $scope, $state, $transition$, $filter, Container, Commit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService) {
|
||||
.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Commit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', 'HttpRequestHelper',
|
||||
function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, ContainerService, ImageHelper, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService, HttpRequestHelper) {
|
||||
$scope.activityTime = 0;
|
||||
$scope.portBindings = [];
|
||||
|
||||
|
@ -16,8 +16,13 @@ function ($q, $scope, $state, $transition$, $filter, Container, Commit, Containe
|
|||
};
|
||||
|
||||
var update = function () {
|
||||
Container.get({id: $transition$.params().id}, function (d) {
|
||||
var container = new ContainerDetailsViewModel(d);
|
||||
var nodeName = $transition$.params().nodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
$scope.nodeName = nodeName;
|
||||
|
||||
ContainerService.container($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
var container = data;
|
||||
$scope.container = container;
|
||||
$scope.container.edit = false;
|
||||
$scope.container.newContainerName = $filter('trimcontainername')(container.Name);
|
||||
|
@ -41,41 +46,105 @@ function ($q, $scope, $state, $transition$, $filter, Container, Commit, Containe
|
|||
}
|
||||
});
|
||||
}
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve container info');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve container info');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.start = function () {
|
||||
Container.start({id: $scope.container.Id}, {}, function (d) {
|
||||
function executeContainerAction(id, action, successMessage, errorMessage) {
|
||||
action(id)
|
||||
.then(function success(data) {
|
||||
Notifications.success(successMessage, id);
|
||||
update();
|
||||
Notifications.success('Container started', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Notifications.error('Failure', e, 'Unable to start container');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.start = function () {
|
||||
var successMessage = 'Container successfully started';
|
||||
var errorMessage = 'Unable to start container';
|
||||
executeContainerAction($transition$.params().id, ContainerService.startContainer, successMessage, errorMessage);
|
||||
};
|
||||
|
||||
$scope.stop = function () {
|
||||
Container.stop({id: $transition$.params().id}, function (d) {
|
||||
update();
|
||||
Notifications.success('Container stopped', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Notifications.error('Failure', e, 'Unable to stop container');
|
||||
});
|
||||
var successMessage = 'Container successfully stopped';
|
||||
var errorMessage = 'Unable to stop container';
|
||||
executeContainerAction($transition$.params().id, ContainerService.stopContainer, successMessage, errorMessage);
|
||||
};
|
||||
|
||||
$scope.kill = function () {
|
||||
Container.kill({id: $transition$.params().id}, function (d) {
|
||||
update();
|
||||
Notifications.success('Container killed', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Notifications.error('Failure', e, 'Unable to kill container');
|
||||
var successMessage = 'Container successfully killed';
|
||||
var errorMessage = 'Unable to kill container';
|
||||
executeContainerAction($transition$.params().id, ContainerService.killContainer, successMessage, errorMessage);
|
||||
};
|
||||
|
||||
$scope.pause = function() {
|
||||
var successMessage = 'Container successfully paused';
|
||||
var errorMessage = 'Unable to pause container';
|
||||
executeContainerAction($transition$.params().id, ContainerService.pauseContainer, successMessage, errorMessage);
|
||||
};
|
||||
|
||||
$scope.unpause = function() {
|
||||
var successMessage = 'Container successfully resumed';
|
||||
var errorMessage = 'Unable to resume container';
|
||||
executeContainerAction($transition$.params().id, ContainerService.resumeContainer, successMessage, errorMessage);
|
||||
};
|
||||
|
||||
$scope.restart = function () {
|
||||
var successMessage = 'Container successfully restarted';
|
||||
var errorMessage = 'Unable to restart container';
|
||||
executeContainerAction($transition$.params().id, ContainerService.restartContainer, successMessage, errorMessage);
|
||||
};
|
||||
|
||||
$scope.renameContainer = function () {
|
||||
var container = $scope.container;
|
||||
ContainerService.renameContainer($transition$.params().id, container.newContainerName)
|
||||
.then(function success(data) {
|
||||
container.Name = container.newContainerName;
|
||||
Notifications.success('Container successfully renamed', container.Name);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
container.newContainerName = container.Name;
|
||||
Notifications.error('Failure', err, 'Unable to rename container');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.container.edit = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) {
|
||||
$scope.state.leaveNetworkInProgress = true;
|
||||
NetworkService.disconnectContainer(networkId, container.Id, false)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Container left network', container.Id);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to disconnect container from network');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.leaveNetworkInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) {
|
||||
$scope.state.joinNetworkInProgress = true;
|
||||
NetworkService.connectContainer(networkId, container.Id)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Container joined network', container.Id);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect container to network');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.joinNetworkInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.commit = function () {
|
||||
var image = $scope.config.Image;
|
||||
var registry = $scope.config.Registry;
|
||||
|
@ -89,25 +158,6 @@ function ($q, $scope, $state, $transition$, $filter, Container, Commit, Containe
|
|||
});
|
||||
};
|
||||
|
||||
$scope.pause = function () {
|
||||
Container.pause({id: $transition$.params().id}, function (d) {
|
||||
update();
|
||||
Notifications.success('Container paused', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Notifications.error('Failure', e, 'Unable to pause container');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.unpause = function () {
|
||||
Container.unpause({id: $transition$.params().id}, function (d) {
|
||||
update();
|
||||
Notifications.success('Container unpaused', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Notifications.error('Failure', e, 'Unable to unpause container');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.confirmRemove = function () {
|
||||
var title = 'You are about to remove a container.';
|
||||
|
@ -122,12 +172,12 @@ function ($q, $scope, $state, $transition$, $filter, Container, Commit, Containe
|
|||
if (result[0]) {
|
||||
cleanAssociatedVolumes = true;
|
||||
}
|
||||
$scope.remove(cleanAssociatedVolumes);
|
||||
removeContainer(cleanAssociatedVolumes);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.remove = function(cleanAssociatedVolumes) {
|
||||
function removeContainer(cleanAssociatedVolumes) {
|
||||
ContainerService.remove($scope.container, cleanAssociatedVolumes)
|
||||
.then(function success() {
|
||||
Notifications.success('Container successfully removed');
|
||||
|
@ -136,71 +186,7 @@ function ($q, $scope, $state, $transition$, $filter, Container, Commit, Containe
|
|||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove container');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.restart = function () {
|
||||
Container.restart({id: $transition$.params().id}, function (d) {
|
||||
update();
|
||||
Notifications.success('Container restarted', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Notifications.error('Failure', e, 'Unable to restart container');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.renameContainer = function () {
|
||||
var container = $scope.container;
|
||||
Container.rename({id: $transition$.params().id, 'name': container.newContainerName}, function (d) {
|
||||
if (d.message) {
|
||||
container.newContainerName = container.Name;
|
||||
Notifications.error('Unable to rename container', {}, d.message);
|
||||
} else {
|
||||
container.Name = container.newContainerName;
|
||||
Notifications.success('Container successfully renamed', container.Name);
|
||||
}
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to rename container');
|
||||
});
|
||||
$scope.container.edit = false;
|
||||
};
|
||||
|
||||
$scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) {
|
||||
$scope.state.leaveNetworkInProgress = true;
|
||||
Network.disconnect({id: networkId}, { Container: $transition$.params().id, Force: false }, function (d) {
|
||||
if (container.message) {
|
||||
Notifications.error('Error', d, 'Unable to disconnect container from network');
|
||||
} else {
|
||||
Notifications.success('Container left network', $transition$.params().id);
|
||||
$state.go('docker.containers.container', {id: $transition$.params().id}, {reload: true});
|
||||
}
|
||||
$scope.state.leaveNetworkInProgress = false;
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to disconnect container from network');
|
||||
$scope.state.leaveNetworkInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.duplicate = function() {
|
||||
$state.go('docker.containers.new', {from: $transition$.params().id}, {reload: true});
|
||||
};
|
||||
|
||||
$scope.confirmRemove = function () {
|
||||
var title = 'You are about to remove a container.';
|
||||
if ($scope.container.State.Running) {
|
||||
title = 'You are about to remove a running container.';
|
||||
}
|
||||
ModalService.confirmContainerDeletion(
|
||||
title,
|
||||
function (result) {
|
||||
if(!result) { return; }
|
||||
var cleanAssociatedVolumes = false;
|
||||
if (result[0]) {
|
||||
cleanAssociatedVolumes = true;
|
||||
}
|
||||
$scope.remove(cleanAssociatedVolumes);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function recreateContainer(pullImage) {
|
||||
var container = $scope.container;
|
||||
|
@ -253,22 +239,6 @@ function ($q, $scope, $state, $transition$, $filter, Container, Commit, Containe
|
|||
});
|
||||
};
|
||||
|
||||
$scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) {
|
||||
$scope.state.joinNetworkInProgress = true;
|
||||
Network.connect({id: networkId}, { Container: $transition$.params().id }, function (d) {
|
||||
if (container.message) {
|
||||
Notifications.error('Error', d, 'Unable to connect container to network');
|
||||
} else {
|
||||
Notifications.success('Container joined network', $transition$.params().id);
|
||||
$state.go('docker.containers.container', {id: $transition$.params().id}, {reload: true});
|
||||
}
|
||||
$scope.state.joinNetworkInProgress = false;
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to connect container to network');
|
||||
$scope.state.joinNetworkInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
NetworkService.networks(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ContainerInspectController', ['$scope', '$transition$', 'Notifications', 'ContainerService',
|
||||
function ($scope, $transition$, Notifications, ContainerService) {
|
||||
.controller('ContainerInspectController', ['$scope', '$transition$', 'Notifications', 'ContainerService', 'HttpRequestHelper',
|
||||
function ($scope, $transition$, Notifications, ContainerService, HttpRequestHelper) {
|
||||
|
||||
$scope.state = {
|
||||
DisplayTextView: false
|
||||
|
@ -8,6 +8,7 @@ function ($scope, $transition$, Notifications, ContainerService) {
|
|||
$scope.containerInfo = {};
|
||||
|
||||
function initView() {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
|
||||
ContainerService.inspect($transition$.params().id)
|
||||
.then(function success(d) {
|
||||
$scope.containerInfo = d;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ContainerLogsController', ['$scope', '$transition$', '$interval', 'ContainerService', 'Notifications',
|
||||
function ($scope, $transition$, $interval, ContainerService, Notifications) {
|
||||
.controller('ContainerLogsController', ['$scope', '$transition$', '$interval', 'ContainerService', 'Notifications', 'HttpRequestHelper',
|
||||
function ($scope, $transition$, $interval, ContainerService, Notifications, HttpRequestHelper) {
|
||||
$scope.state = {
|
||||
refreshRate: 3,
|
||||
lineCount: 2000,
|
||||
|
@ -58,6 +58,7 @@ function ($scope, $transition$, $interval, ContainerService, Notifications) {
|
|||
}
|
||||
|
||||
function initView() {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
|
||||
ContainerService.container($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
var container = data;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ContainerStatsController', ['$q', '$scope', '$transition$', '$document', '$interval', 'ContainerService', 'ChartService', 'Notifications',
|
||||
function ($q, $scope, $transition$, $document, $interval, ContainerService, ChartService, Notifications) {
|
||||
.controller('ContainerStatsController', ['$q', '$scope', '$transition$', '$document', '$interval', 'ContainerService', 'ChartService', 'Notifications', 'HttpRequestHelper',
|
||||
function ($q, $scope, $transition$, $document, $interval, ContainerService, ChartService, Notifications, HttpRequestHelper) {
|
||||
|
||||
$scope.state = {
|
||||
refreshRate: '5',
|
||||
|
@ -126,6 +126,7 @@ function ($q, $scope, $transition$, $document, $interval, ContainerService, Char
|
|||
}
|
||||
|
||||
function initView() {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
|
||||
ContainerService.container($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
$scope.container = data;
|
||||
|
|
|
@ -3,7 +3,13 @@
|
|||
<rd-header-content>Dashboard</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" ng-if="applicationState.endpoint.mode.agentProxy">
|
||||
<div class="col-sm-12">
|
||||
<dashboard-cluster-agent-info></dashboard-cluster-agent-info>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="!applicationState.endpoint.mode.agentProxy">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer-alt" title="Node info"></rd-widget-header>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('BuildImageController', ['$scope', '$state', 'BuildService', 'Notifications',
|
||||
function ($scope, $state, BuildService, Notifications) {
|
||||
.controller('BuildImageController', ['$scope', '$state', 'BuildService', 'Notifications', 'HttpRequestHelper',
|
||||
function ($scope, $state, BuildService, Notifications, HttpRequestHelper) {
|
||||
|
||||
$scope.state = {
|
||||
BuildType: 'editor',
|
||||
|
@ -13,7 +13,8 @@ function ($scope, $state, BuildService, Notifications) {
|
|||
UploadFile: null,
|
||||
DockerFileContent: '',
|
||||
URL: '',
|
||||
Path: 'Dockerfile'
|
||||
Path: 'Dockerfile',
|
||||
NodeName: null
|
||||
};
|
||||
|
||||
$scope.addImageName = function() {
|
||||
|
@ -56,6 +57,9 @@ function ($scope, $state, BuildService, Notifications) {
|
|||
return x.Name;
|
||||
});
|
||||
|
||||
var nodeName = $scope.formValues.NodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
|
||||
buildImageBasedOnBuildType(buildType, imageNames)
|
||||
.then(function success(data) {
|
||||
$scope.buildLogs = data.buildLogs;
|
||||
|
|
|
@ -194,6 +194,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !url -->
|
||||
<div ng-if="applicationState.endpoint.mode.agentProxy">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment
|
||||
</div>
|
||||
<!-- node-selection -->
|
||||
<node-selector
|
||||
model="formValues.NodeName">
|
||||
</node-selector>
|
||||
<!-- !node-selection -->
|
||||
</div>
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ImageController', ['$q', '$scope', '$transition$', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications',
|
||||
function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryService, Notifications) {
|
||||
.controller('ImageController', ['$q', '$scope', '$transition$', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications', 'HttpRequestHelper',
|
||||
function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryService, Notifications, HttpRequestHelper) {
|
||||
$scope.formValues = {
|
||||
Image: '',
|
||||
Registry: ''
|
||||
|
@ -98,6 +98,7 @@ function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryServ
|
|||
};
|
||||
|
||||
function initView() {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
|
||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||
$q.all({
|
||||
image: ImageService.image($transition$.params().id),
|
||||
|
|
|
@ -26,6 +26,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !tag-note -->
|
||||
<div ng-if="applicationState.endpoint.mode.agentProxy">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment
|
||||
</div>
|
||||
<!-- node-selection -->
|
||||
<node-selector
|
||||
model="formValues.NodeName">
|
||||
</node-selector>
|
||||
<!-- !node-selection -->
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.Image" ng-click="pullImage()" button-spinner="state.actionInProgress">
|
||||
|
@ -46,6 +56,7 @@
|
|||
title="Images" title-icon="fa-clone"
|
||||
dataset="images" table-key="images"
|
||||
order-by="RepoTags" show-text-filter="true"
|
||||
show-host-column="applicationState.endpoint.mode.agentProxy"
|
||||
remove-action="removeAction"
|
||||
force-remove-action="confirmRemovalAction"
|
||||
></images-datatable>
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'ModalService',
|
||||
function ($scope, $state, ImageService, Notifications, ModalService) {
|
||||
.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'ModalService', 'HttpRequestHelper',
|
||||
function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper) {
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Image: '',
|
||||
Registry: ''
|
||||
Registry: '',
|
||||
NodeName: null
|
||||
};
|
||||
|
||||
$scope.pullImage = function() {
|
||||
var image = $scope.formValues.Image;
|
||||
var registry = $scope.formValues.Registry;
|
||||
|
||||
var nodeName = $scope.formValues.NodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
ImageService.pullImage(image, registry, false)
|
||||
.then(function success(data) {
|
||||
|
@ -38,6 +42,7 @@ function ($scope, $state, ImageService, Notifications, ModalService) {
|
|||
$scope.removeAction = function (selectedItems, force) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (image) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(image.NodeName);
|
||||
ImageService.deleteImage(image.Id, force)
|
||||
.then(function success() {
|
||||
Notifications.success('Image successfully removed', image.Id);
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'NetworkService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
|
||||
function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
|
||||
.controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'NetworkService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator', 'HttpRequestHelper',
|
||||
function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator, HttpRequestHelper) {
|
||||
|
||||
$scope.formValues = {
|
||||
DriverOptions: [],
|
||||
Subnet: '',
|
||||
Gateway: '',
|
||||
Labels: [],
|
||||
AccessControlData: new AccessControlFormData()
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
NodeName: null
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
|
@ -99,6 +100,9 @@ function ($q, $scope, $state, PluginService, Notifications, NetworkService, Labe
|
|||
return;
|
||||
}
|
||||
|
||||
var nodeName = $scope.formValues.NodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
NetworkService.create(networkConfiguration)
|
||||
.then(function success(data) {
|
||||
|
|
|
@ -119,6 +119,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !internal -->
|
||||
<div ng-if="applicationState.endpoint.mode.agentProxy && config.Driver !== 'overlay'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment
|
||||
</div>
|
||||
<!-- node-selection -->
|
||||
<node-selector
|
||||
model="formValues.NodeName">
|
||||
</node-selector>
|
||||
<!-- !node-selection -->
|
||||
</div>
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
|
|
|
@ -89,12 +89,12 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="container in containersInNetwork">
|
||||
<td><a ui-sref="docker.containers.container({id: container.Id})">{{ container.Name }}</a></td>
|
||||
<td><a ui-sref="docker.containers.container({ id: container.Id, nodeName: nodeName })">{{ container.Name }}</a></td>
|
||||
<td>{{ container.IPv4Address || '-' }}</td>
|
||||
<td>{{ container.IPv6Address || '-' }}</td>
|
||||
<td>{{ container.MacAddress || '-' }}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(network, container.Id)"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Leave Network</button>
|
||||
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(network, container)"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Leave Network</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'Network', 'NetworkService', 'Container', 'ContainerHelper', 'Notifications',
|
||||
function ($scope, $state, $transition$, $filter, Network, NetworkService, Container, ContainerHelper, Notifications) {
|
||||
.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'ContainerHelper', 'Notifications', 'HttpRequestHelper',
|
||||
function ($scope, $state, $transition$, $filter, NetworkService, Container, ContainerHelper, Notifications, HttpRequestHelper) {
|
||||
|
||||
$scope.removeNetwork = function removeNetwork(networkId) {
|
||||
Network.remove({id: $transition$.params().id}, function (d) {
|
||||
if (d.message) {
|
||||
Notifications.error('Error', d, 'Unable to remove network');
|
||||
} else {
|
||||
Notifications.success('Network removed', $transition$.params().id);
|
||||
$state.go('docker.networks', {});
|
||||
}
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to remove network');
|
||||
NetworkService.remove($transition$.params().id, $transition$.params().id)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Network removed', $transition$.params().id);
|
||||
$state.go('docker.networks', {});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove network');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.containerLeaveNetwork = function containerLeaveNetwork(network, containerId) {
|
||||
Network.disconnect({id: $transition$.params().id}, { Container: containerId, Force: false }, function (d) {
|
||||
if (d.message) {
|
||||
Notifications.error('Error', d, 'Unable to disconnect container from network');
|
||||
} else {
|
||||
Notifications.success('Container left network', $transition$.params().id);
|
||||
$state.go('docker.networks.network', {id: network.Id}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to disconnect container from network');
|
||||
$scope.containerLeaveNetwork = function containerLeaveNetwork(network, container) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName);
|
||||
NetworkService.disconnectContainer($transition$.params().id, container.Id, false)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Container left network', $transition$.params().id);
|
||||
$state.go('docker.networks.network', { id: network.Id }, { reload: true });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to disconnect container from network');
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -71,6 +68,9 @@ function ($scope, $state, $transition$, $filter, Network, NetworkService, Contai
|
|||
}
|
||||
|
||||
function initView() {
|
||||
var nodeName = $transition$.params().nodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
$scope.nodeName = nodeName;
|
||||
NetworkService.network($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
$scope.network = data;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
order-by="Name" show-text-filter="true"
|
||||
remove-action="removeAction"
|
||||
show-ownership-column="applicationState.application.authentication"
|
||||
show-host-column="applicationState.endpoint.mode.agentProxy"
|
||||
></networks-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('NetworksController', ['$scope', '$state', 'NetworkService', 'Notifications',
|
||||
function ($scope, $state, NetworkService, Notifications) {
|
||||
.controller('NetworksController', ['$scope', '$state', 'NetworkService', 'Notifications', 'HttpRequestHelper',
|
||||
function ($scope, $state, NetworkService, Notifications, HttpRequestHelper) {
|
||||
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (network) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(network.NodeName);
|
||||
NetworkService.remove(network.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Network successfully removed', network.Name);
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
show-text-filter="true"
|
||||
show-slot-column="service.Mode !== 'global'"
|
||||
show-logs-button="applicationState.endpoint.apiVersion >= 1.30"
|
||||
agent-powered="applicationState.endpoint.mode.agentProxy"
|
||||
></tasks-datatable>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'ModalService', 'PluginService', 'Authentication', 'SettingsService', 'VolumeService',
|
||||
function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, ModalService, PluginService, Authentication, SettingsService, VolumeService) {
|
||||
.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'ContainerService', 'TaskHelper', 'Notifications', 'ModalService', 'PluginService', 'Authentication', 'SettingsService', 'VolumeService',
|
||||
function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, ContainerService, TaskHelper, Notifications, ModalService, PluginService, Authentication, SettingsService, VolumeService) {
|
||||
|
||||
$scope.state = {
|
||||
updateInProgress: false,
|
||||
|
@ -407,6 +407,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
|
||||
function initView() {
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
|
||||
|
||||
ServiceService.service($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
|
@ -425,6 +426,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
return $q.all({
|
||||
volumes: VolumeService.volumes(),
|
||||
tasks: TaskService.tasks({ service: [service.Name] }),
|
||||
containers: agentProxy ? ContainerService.containers() : [],
|
||||
nodes: NodeService.nodes(),
|
||||
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
||||
configs: apiVersion >= 1.30 ? ConfigService.configs() : [],
|
||||
|
@ -434,7 +436,6 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
});
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.tasks = data.tasks;
|
||||
$scope.nodes = data.nodes;
|
||||
$scope.configs = data.configs;
|
||||
$scope.secrets = data.secrets;
|
||||
|
@ -445,6 +446,19 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
var userDetails = Authentication.getUserDetails();
|
||||
$scope.isAdmin = userDetails.role === 1;
|
||||
|
||||
var tasks = data.tasks;
|
||||
|
||||
if (agentProxy) {
|
||||
var containers = data.containers;
|
||||
for (var i = 0; i < tasks.length; i++) {
|
||||
var task = tasks[i];
|
||||
TaskHelper.associateContainerToTask(task, containers);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.tasks = data.tasks;
|
||||
|
||||
|
||||
// Set max cpu value
|
||||
var maxCpus = 0;
|
||||
for (var n in data.nodes) {
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
show-text-filter="true"
|
||||
show-slot-column="true"
|
||||
show-logs-button="applicationState.endpoint.apiVersion >= 1.30"
|
||||
agent-powered="applicationState.endpoint.mode.agentProxy"
|
||||
></tasks-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('StackController', ['$q', '$scope', '$state', '$transition$', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ServiceHelper', 'Notifications', 'FormHelper', 'EndpointProvider',
|
||||
function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceService, TaskService, ServiceHelper, Notifications, FormHelper, EndpointProvider) {
|
||||
.controller('StackController', ['$q', '$scope', '$state', '$transition$', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ContainerService', 'ServiceHelper', 'TaskHelper', 'Notifications', 'FormHelper', 'EndpointProvider',
|
||||
function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceService, TaskService, ContainerService, ServiceHelper, TaskHelper, Notifications, FormHelper, EndpointProvider) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
|
@ -41,6 +41,7 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
|||
function initView() {
|
||||
var stackId = $transition$.params().id;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
|
||||
|
||||
StackService.stack(stackId)
|
||||
.then(function success(data) {
|
||||
|
@ -55,24 +56,31 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
|||
stackFile: StackService.getStackFile(stackId),
|
||||
services: ServiceService.services(serviceFilters),
|
||||
tasks: TaskService.tasks(serviceFilters),
|
||||
containers: agentProxy ? ContainerService.containers() : [],
|
||||
nodes: NodeService.nodes()
|
||||
});
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.stackFileContent = data.stackFile;
|
||||
|
||||
$scope.nodes = data.nodes;
|
||||
|
||||
var services = data.services;
|
||||
|
||||
var tasks = data.tasks;
|
||||
$scope.tasks = tasks;
|
||||
|
||||
for (var i = 0; i < services.length; i++) {
|
||||
var service = services[i];
|
||||
ServiceHelper.associateTasksToService(service, tasks);
|
||||
}
|
||||
|
||||
if (agentProxy) {
|
||||
var containers = data.containers;
|
||||
for (var j = 0; j < tasks.length; j++) {
|
||||
var task = tasks[j];
|
||||
TaskHelper.associateContainerToTask(task, containers);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.tasks = tasks;
|
||||
$scope.services = services;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator',
|
||||
function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator) {
|
||||
.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator', 'HttpRequestHelper',
|
||||
function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator, HttpRequestHelper) {
|
||||
|
||||
$scope.formValues = {
|
||||
Driver: 'local',
|
||||
DriverOptions: [],
|
||||
AccessControlData: new AccessControlFormData()
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
NodeName: null
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
|
@ -55,6 +56,9 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi
|
|||
return;
|
||||
}
|
||||
|
||||
var nodeName = $scope.formValues.NodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
VolumeService.createVolume(volumeConfiguration)
|
||||
.then(function success(data) {
|
||||
|
@ -76,7 +80,7 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi
|
|||
|
||||
function initView() {
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
var endpointProvider = $scope.applicationState.endpoint.provider;
|
||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||
|
||||
PluginService.volumePlugins(apiVersion < 1.25 || endpointProvider === 'VMWARE_VIC')
|
||||
.then(function success(data) {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
|
@ -70,6 +70,16 @@
|
|||
<storidge-profile-selector storidge-profile="formValues.StoridgeProfile"></storidge-profile-selector>
|
||||
</div>
|
||||
<!-- storidge -->
|
||||
<div ng-if="applicationState.endpoint.mode.agentProxy && formValues.Driver === 'local'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment
|
||||
</div>
|
||||
<!-- node-selection -->
|
||||
<node-selector
|
||||
model="formValues.NodeName">
|
||||
</node-selector>
|
||||
<!-- !node-selection -->
|
||||
</div>
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="container in containersUsingVolume">
|
||||
<td><a ui-sref="docker.containers.container({id: container.Id})">{{ container | containername }}</a></td>
|
||||
<td><a ui-sref="docker.containers.container({ id: container.Id, nodeName: container.NodeName })">{{ container | containername }}</a></td>
|
||||
<td>{{ container.volumeData.Destination }}</td>
|
||||
<td>{{ !container.volumeData.RW }}</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('VolumeController', ['$scope', '$state', '$transition$', 'VolumeService', 'ContainerService', 'Notifications',
|
||||
function ($scope, $state, $transition$, VolumeService, ContainerService, Notifications) {
|
||||
.controller('VolumeController', ['$scope', '$state', '$transition$', 'VolumeService', 'ContainerService', 'Notifications', 'HttpRequestHelper',
|
||||
function ($scope, $state, $transition$, VolumeService, ContainerService, Notifications, HttpRequestHelper) {
|
||||
|
||||
$scope.removeVolume = function removeVolume() {
|
||||
VolumeService.remove($scope.volume)
|
||||
|
@ -20,6 +20,7 @@ function ($scope, $state, $transition$, VolumeService, ContainerService, Notific
|
|||
}
|
||||
|
||||
function initView() {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
|
||||
VolumeService.volume($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
var volume = data;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
order-by="Id" show-text-filter="true"
|
||||
remove-action="removeAction"
|
||||
show-ownership-column="applicationState.application.authentication"
|
||||
show-host-column="applicationState.endpoint.mode.agentProxy"
|
||||
></volumes-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue