Compare commits
27 Commits
1.19.2
...
feat1752-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58afb8be3d | ||
|
|
d6ba46ed7f | ||
|
|
0e5fbf298e | ||
|
|
c5aecfe6f3 | ||
|
|
5341ad33af | ||
|
|
e948d606f4 | ||
|
|
ca08b2fa2a | ||
|
|
275fcf5587 | ||
|
|
3422662191 | ||
|
|
f6d9a4c7c1 | ||
|
|
575735a6f7 | ||
|
|
b7c48fcbed | ||
|
|
6e8a10d72f | ||
|
|
bad95987ec | ||
|
|
9b4870d57e | ||
|
|
6e262e6e89 | ||
|
|
5be2684442 | ||
|
|
226c45f035 | ||
|
|
92b15523f0 | ||
|
|
f0f01c33bd | ||
|
|
94b202fedc | ||
|
|
d5dd362d53 | ||
|
|
c3d80a1b21 | ||
|
|
b192b098ca | ||
|
|
22450bbdeb | ||
|
|
313c8be997 | ||
|
|
885c61fb7b |
@@ -77,14 +77,14 @@ The subject contains succinct description of the change:
|
||||
|
||||
## Contribution process
|
||||
|
||||
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `contrib/` labels, such as `contrib/func-review-in-progress` or `contrib/tech-review-approved`.
|
||||
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
|
||||
|
||||
### Bug report
|
||||
|
||||

|
||||

|
||||
|
||||
### Feature request
|
||||
|
||||
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation.
|
||||
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase.
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/deploykey"
|
||||
"github.com/portainer/portainer/bolt/dockerhub"
|
||||
"github.com/portainer/portainer/bolt/endpoint"
|
||||
"github.com/portainer/portainer/bolt/endpointgroup"
|
||||
@@ -49,6 +50,7 @@ type Store struct {
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
DeploykeyService *deploykey.Service
|
||||
}
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
@@ -204,6 +206,12 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.TagService = tagService
|
||||
|
||||
deploykeyService, err := deploykey.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.DeploykeyService = deploykeyService
|
||||
|
||||
teammembershipService, err := teammembership.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
75
api/bolt/deploykey/deploykey.go
Normal file
75
api/bolt/deploykey/deploykey.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package deploykey
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "deploykeys"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Keys return an array containing all the keys.
|
||||
func (service *Service) Deploykeys() ([]portainer.Deploykey, error) {
|
||||
var deploykeys = make([]portainer.Deploykey, 0)
|
||||
|
||||
err := service.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var deploykey portainer.Deploykey
|
||||
err := internal.UnmarshalObject(v, &deploykey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deploykeys = append(deploykeys, deploykey)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return deploykeys, err
|
||||
}
|
||||
|
||||
// CreateKey creates a new key.
|
||||
func (service *Service) CreateDeploykey(deploykey *portainer.Deploykey) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
deploykey.ID = portainer.DeploykeyID(id)
|
||||
|
||||
data, err := internal.MarshalObject(deploykey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(deploykey.ID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteDeploykey deletes a key.
|
||||
func (service *Service) DeleteDeploykey(ID portainer.DeploykeyID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
}
|
||||
@@ -94,6 +94,11 @@ func initCryptoService() portainer.CryptoService {
|
||||
return &crypto.Service{}
|
||||
}
|
||||
|
||||
func initDigitalDeploykeyService() portainer.DigitalDeploykeyService {
|
||||
return &crypto.ECDSAService{}
|
||||
}
|
||||
|
||||
|
||||
func initLDAPService() portainer.LDAPService {
|
||||
return &ldap.Service{}
|
||||
}
|
||||
@@ -401,6 +406,8 @@ func main() {
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
digitalDeploykeyService := initDigitalDeploykeyService()
|
||||
|
||||
err := initKeyPair(fileService, digitalSignatureService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -498,6 +505,8 @@ func main() {
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
DeploykeyService: store.DeploykeyService,
|
||||
DigitalDeploykeyService: digitalDeploykeyService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
RegistryService: store.RegistryService,
|
||||
|
||||
@@ -45,11 +45,7 @@ func (scheduler *JobScheduler) ScheduleEndpointSyncJob(endpointFilePath string,
|
||||
// ScheduleSnapshotJob schedules a cron job to create endpoint snapshots
|
||||
func (scheduler *JobScheduler) ScheduleSnapshotJob(interval string) error {
|
||||
job := newEndpointSnapshotJob(scheduler.endpointService, scheduler.snapshotter)
|
||||
|
||||
err := job.Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go job.Snapshot()
|
||||
|
||||
return scheduler.cron.AddJob("@every "+interval, job)
|
||||
}
|
||||
|
||||
378
api/crypto/certs.go
Normal file
378
api/crypto/certs.go
Normal file
@@ -0,0 +1,378 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// These constants from [PROTOCOL.certkeys] represent the algorithm names
|
||||
// for certificate types supported by this package.
|
||||
const (
|
||||
CertAlgoRSAv01 = "ssh-rsa-cert-v01@openssh.com"
|
||||
CertAlgoDSAv01 = "ssh-dss-cert-v01@openssh.com"
|
||||
CertAlgoECDSA256v01 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
|
||||
CertAlgoECDSA384v01 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
|
||||
CertAlgoECDSA521v01 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
|
||||
)
|
||||
|
||||
// Certificate types are used to specify whether a certificate is for identification
|
||||
// of a user or a host. Current identities are defined in [PROTOCOL.certkeys].
|
||||
const (
|
||||
UserCert = 1
|
||||
HostCert = 2
|
||||
)
|
||||
|
||||
type signature struct {
|
||||
Format string
|
||||
Blob []byte
|
||||
}
|
||||
|
||||
type tuple struct {
|
||||
Name string
|
||||
Data string
|
||||
}
|
||||
|
||||
const (
|
||||
maxUint64 = 1<<64 - 1
|
||||
maxInt64 = 1<<63 - 1
|
||||
)
|
||||
|
||||
// CertTime represents an unsigned 64-bit time value in seconds starting from
|
||||
// UNIX epoch. We use CertTime instead of time.Time in order to properly handle
|
||||
// the "infinite" time value ^0, which would become negative when expressed as
|
||||
// an int64.
|
||||
type CertTime uint64
|
||||
|
||||
func (ct CertTime) Time() time.Time {
|
||||
if ct > maxInt64 {
|
||||
return time.Unix(maxInt64, 0)
|
||||
}
|
||||
return time.Unix(int64(ct), 0)
|
||||
}
|
||||
|
||||
func (ct CertTime) IsInfinite() bool {
|
||||
return ct == maxUint64
|
||||
}
|
||||
|
||||
// An OpenSSHCertV01 represents an OpenSSH certificate as defined in
|
||||
// [PROTOCOL.certkeys]?rev=1.8.
|
||||
type OpenSSHCertV01 struct {
|
||||
Nonce []byte
|
||||
Key PublicKey
|
||||
Serial uint64
|
||||
Type uint32
|
||||
KeyId string
|
||||
ValidPrincipals []string
|
||||
ValidAfter, ValidBefore CertTime
|
||||
CriticalOptions []tuple
|
||||
Extensions []tuple
|
||||
Reserved []byte
|
||||
SignatureKey PublicKey
|
||||
Signature *signature
|
||||
}
|
||||
|
||||
// validateOpenSSHCertV01Signature uses the cert's SignatureKey to verify that
|
||||
// the cert's Signature.Blob is the result of signing the cert bytes starting
|
||||
// from the algorithm string and going up to and including the SignatureKey.
|
||||
func validateOpenSSHCertV01Signature(cert *OpenSSHCertV01) bool {
|
||||
return cert.SignatureKey.Verify(cert.BytesForSigning(), cert.Signature.Blob)
|
||||
}
|
||||
|
||||
var certAlgoNames = map[string]string{
|
||||
KeyAlgoRSA: CertAlgoRSAv01,
|
||||
KeyAlgoDSA: CertAlgoDSAv01,
|
||||
KeyAlgoECDSA256: CertAlgoECDSA256v01,
|
||||
KeyAlgoECDSA384: CertAlgoECDSA384v01,
|
||||
KeyAlgoECDSA521: CertAlgoECDSA521v01,
|
||||
}
|
||||
|
||||
// certToPrivAlgo returns the underlying algorithm for a certificate algorithm.
|
||||
// Panics if a non-certificate algorithm is passed.
|
||||
func certToPrivAlgo(algo string) string {
|
||||
for privAlgo, pubAlgo := range certAlgoNames {
|
||||
if pubAlgo == algo {
|
||||
return privAlgo
|
||||
}
|
||||
}
|
||||
panic("unknown cert algorithm")
|
||||
}
|
||||
|
||||
func (cert *OpenSSHCertV01) marshal(includeAlgo, includeSig bool) []byte {
|
||||
algoName := cert.PublicKeyAlgo()
|
||||
pubKey := cert.Key.Marshal()
|
||||
sigKey := MarshalPublicKey(cert.SignatureKey)
|
||||
|
||||
var length int
|
||||
if includeAlgo {
|
||||
length += stringLength(len(algoName))
|
||||
}
|
||||
length += stringLength(len(cert.Nonce))
|
||||
length += len(pubKey)
|
||||
length += 8 // Length of Serial
|
||||
length += 4 // Length of Type
|
||||
length += stringLength(len(cert.KeyId))
|
||||
length += lengthPrefixedNameListLength(cert.ValidPrincipals)
|
||||
length += 8 // Length of ValidAfter
|
||||
length += 8 // Length of ValidBefore
|
||||
length += tupleListLength(cert.CriticalOptions)
|
||||
length += tupleListLength(cert.Extensions)
|
||||
length += stringLength(len(cert.Reserved))
|
||||
length += stringLength(len(sigKey))
|
||||
if includeSig {
|
||||
length += signatureLength(cert.Signature)
|
||||
}
|
||||
|
||||
ret := make([]byte, length)
|
||||
r := ret
|
||||
if includeAlgo {
|
||||
r = marshalString(r, []byte(algoName))
|
||||
}
|
||||
r = marshalString(r, cert.Nonce)
|
||||
copy(r, pubKey)
|
||||
r = r[len(pubKey):]
|
||||
r = marshalUint64(r, cert.Serial)
|
||||
r = marshalUint32(r, cert.Type)
|
||||
r = marshalString(r, []byte(cert.KeyId))
|
||||
r = marshalLengthPrefixedNameList(r, cert.ValidPrincipals)
|
||||
r = marshalUint64(r, uint64(cert.ValidAfter))
|
||||
r = marshalUint64(r, uint64(cert.ValidBefore))
|
||||
r = marshalTupleList(r, cert.CriticalOptions)
|
||||
r = marshalTupleList(r, cert.Extensions)
|
||||
r = marshalString(r, cert.Reserved)
|
||||
r = marshalString(r, sigKey)
|
||||
if includeSig {
|
||||
r = marshalSignature(r, cert.Signature)
|
||||
}
|
||||
if len(r) > 0 {
|
||||
panic("ssh: internal error, marshaling certificate did not fill the entire buffer")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (cert *OpenSSHCertV01) BytesForSigning() []byte {
|
||||
return cert.marshal(true, false)
|
||||
}
|
||||
|
||||
func (cert *OpenSSHCertV01) Marshal() []byte {
|
||||
return cert.marshal(false, true)
|
||||
}
|
||||
|
||||
func (c *OpenSSHCertV01) PublicKeyAlgo() string {
|
||||
algo, ok := certAlgoNames[c.Key.PublicKeyAlgo()]
|
||||
if !ok {
|
||||
panic("unknown cert key type")
|
||||
}
|
||||
return algo
|
||||
}
|
||||
|
||||
func (c *OpenSSHCertV01) PrivateKeyAlgo() string {
|
||||
return c.Key.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
func (c *OpenSSHCertV01) Verify(data []byte, sig []byte) bool {
|
||||
return c.Key.Verify(data, sig)
|
||||
}
|
||||
|
||||
func parseOpenSSHCertV01(in []byte, algo string) (out *OpenSSHCertV01, rest []byte, ok bool) {
|
||||
cert := new(OpenSSHCertV01)
|
||||
|
||||
if cert.Nonce, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
privAlgo := certToPrivAlgo(algo)
|
||||
cert.Key, in, ok = parsePubKey(in, privAlgo)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// We test PublicKeyAlgo to make sure we don't use some weird sub-cert.
|
||||
if cert.Key.PublicKeyAlgo() != privAlgo {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Serial, in, ok = parseUint64(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Type, in, ok = parseUint32(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
keyId, in, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cert.KeyId = string(keyId)
|
||||
|
||||
if cert.ValidPrincipals, in, ok = parseLengthPrefixedNameList(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
va, in, ok := parseUint64(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cert.ValidAfter = CertTime(va)
|
||||
|
||||
vb, in, ok := parseUint64(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cert.ValidBefore = CertTime(vb)
|
||||
|
||||
if cert.CriticalOptions, in, ok = parseTupleList(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Extensions, in, ok = parseTupleList(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Reserved, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
sigKey, in, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if cert.SignatureKey, _, ok = ParsePublicKey(sigKey); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Signature, in, ok = parseSignature(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return cert, in, ok
|
||||
}
|
||||
|
||||
func lengthPrefixedNameListLength(namelist []string) int {
|
||||
length := 4 // length prefix for list
|
||||
for _, name := range namelist {
|
||||
length += 4 // length prefix for name
|
||||
length += len(name)
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalLengthPrefixedNameList(to []byte, namelist []string) []byte {
|
||||
length := uint32(lengthPrefixedNameListLength(namelist) - 4)
|
||||
to = marshalUint32(to, length)
|
||||
for _, name := range namelist {
|
||||
to = marshalString(to, []byte(name))
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
func parseLengthPrefixedNameList(in []byte) (out []string, rest []byte, ok bool) {
|
||||
list, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for len(list) > 0 {
|
||||
var next []byte
|
||||
if next, list, ok = parseString(list); !ok {
|
||||
return nil, nil, false
|
||||
}
|
||||
out = append(out, string(next))
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func tupleListLength(tupleList []tuple) int {
|
||||
length := 4 // length prefix for list
|
||||
for _, t := range tupleList {
|
||||
length += 4 // length prefix for t.Name
|
||||
length += len(t.Name)
|
||||
length += 4 // length prefix for t.Data
|
||||
length += len(t.Data)
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalTupleList(to []byte, tuplelist []tuple) []byte {
|
||||
length := uint32(tupleListLength(tuplelist) - 4)
|
||||
to = marshalUint32(to, length)
|
||||
for _, t := range tuplelist {
|
||||
to = marshalString(to, []byte(t.Name))
|
||||
to = marshalString(to, []byte(t.Data))
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
func parseTupleList(in []byte) (out []tuple, rest []byte, ok bool) {
|
||||
list, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for len(list) > 0 {
|
||||
var name, data []byte
|
||||
var ok bool
|
||||
name, list, ok = parseString(list)
|
||||
if !ok {
|
||||
return nil, nil, false
|
||||
}
|
||||
data, list, ok = parseString(list)
|
||||
if !ok {
|
||||
return nil, nil, false
|
||||
}
|
||||
out = append(out, tuple{string(name), string(data)})
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func signatureLength(sig *signature) int {
|
||||
length := 4 // length prefix for signature
|
||||
length += stringLength(len(sig.Format))
|
||||
length += stringLength(len(sig.Blob))
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalSignature(to []byte, sig *signature) []byte {
|
||||
length := uint32(signatureLength(sig) - 4)
|
||||
to = marshalUint32(to, length)
|
||||
to = marshalString(to, []byte(sig.Format))
|
||||
to = marshalString(to, sig.Blob)
|
||||
return to
|
||||
}
|
||||
|
||||
func parseSignatureBody(in []byte) (out *signature, rest []byte, ok bool) {
|
||||
var format []byte
|
||||
if format, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
out = &signature{
|
||||
Format: string(format),
|
||||
}
|
||||
|
||||
if out.Blob, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
return out, in, ok
|
||||
}
|
||||
|
||||
func parseSignature(in []byte) (out *signature, rest []byte, ok bool) {
|
||||
var sigBytes []byte
|
||||
if sigBytes, rest, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
out, sigBytes, ok = parseSignatureBody(sigBytes)
|
||||
if !ok || len(sigBytes) > 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package crypto
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -91,6 +93,29 @@ func (service *ECDSAService) GenerateKeyPair() ([]byte, []byte, error) {
|
||||
return private, public, nil
|
||||
}
|
||||
|
||||
// GenerateKeyPair will create a new key pair using ECDSA.
|
||||
func (service *ECDSAService) GenerateSshKey() ([]byte, error) {
|
||||
//savePublicFileTo := "./id_ecdsa_test.pub"
|
||||
|
||||
pubkeyCurve := elliptic.P256()
|
||||
|
||||
byteKeys, err := ecdsa.GenerateKey(pubkeyCurve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
|
||||
publicKeyBytes, err := generateECDSAPublicKey(&byteKeys.PublicKey)
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
log.Println(publicKeyBytes)
|
||||
err = writeKeyToFile(publicKeyBytes,"./testFile" )
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
return publicKeyBytes, nil
|
||||
}
|
||||
|
||||
// Sign creates a signature from a message.
|
||||
// It automatically hash the message using MD5 and creates a signature from
|
||||
// that hash.
|
||||
@@ -120,3 +145,22 @@ func (service *ECDSAService) Sign(message string) (string, error) {
|
||||
|
||||
return base64.RawStdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
func generateECDSAPublicKey(privatekey *ecdsa.PublicKey) ([]byte, error) {
|
||||
publicRsaKey, err := NewPublicKey(privatekey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pubKeyBytes := MarshalAuthorizedKey(publicRsaKey)
|
||||
return pubKeyBytes, nil
|
||||
}
|
||||
// writePemToFile writes keys to a file
|
||||
func writeKeyToFile(keyBytes []byte, saveFileTo string) error {
|
||||
log.Println("hey")
|
||||
err := ioutil.WriteFile(saveFileTo, keyBytes, 0777)
|
||||
log.Println("here")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
615
api/crypto/keys.go
Normal file
615
api/crypto/keys.go
Normal file
@@ -0,0 +1,615 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This package is a duplicate of 32844aa1ae54: https://code.google.com/p/go/source/browse/ssh/keys.go?repo=crypto
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// These constants represent the algorithm names for key types supported by this
|
||||
// package.
|
||||
const (
|
||||
KeyAlgoRSA = "ssh-rsa"
|
||||
KeyAlgoDSA = "ssh-dss"
|
||||
KeyAlgoECDSA256 = "ecdsa-sha2-nistp256"
|
||||
KeyAlgoECDSA384 = "ecdsa-sha2-nistp384"
|
||||
KeyAlgoECDSA521 = "ecdsa-sha2-nistp521"
|
||||
)
|
||||
|
||||
// parsePubKey parses a public key of the given algorithm.
|
||||
// Use ParsePublicKey for keys with prepended algorithm.
|
||||
func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, ok bool) {
|
||||
|
||||
switch algo {
|
||||
case KeyAlgoRSA:
|
||||
return parseRSA(in)
|
||||
case KeyAlgoDSA:
|
||||
return parseDSA(in)
|
||||
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
|
||||
return parseECDSA(in)
|
||||
case CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01:
|
||||
return parseOpenSSHCertV01(in, algo)
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
|
||||
|
||||
// parseAuthorizedKey parses a public key in OpenSSH authorized_keys format
|
||||
// (see sshd(8) manual page) once the options and key type fields have been
|
||||
// removed.
|
||||
func parseAuthorizedKey(in []byte) (out PublicKey, comment string, ok bool) {
|
||||
in = bytes.TrimSpace(in)
|
||||
|
||||
i := bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
i = len(in)
|
||||
}
|
||||
base64Key := in[:i]
|
||||
|
||||
key := make([]byte, base64.StdEncoding.DecodedLen(len(base64Key)))
|
||||
n, err := base64.StdEncoding.Decode(key, base64Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key = key[:n]
|
||||
out, _, ok = ParsePublicKey(key)
|
||||
if !ok {
|
||||
return nil, "", false
|
||||
}
|
||||
comment = string(bytes.TrimSpace(in[i:]))
|
||||
return
|
||||
}
|
||||
|
||||
// ParseAuthorizedKeys parses a public key from an authorized_keys
|
||||
// file used in OpenSSH according to the sshd(8) manual page.
|
||||
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, ok bool) {
|
||||
for len(in) > 0 {
|
||||
end := bytes.IndexByte(in, '\n')
|
||||
if end != -1 {
|
||||
rest = in[end+1:]
|
||||
in = in[:end]
|
||||
} else {
|
||||
rest = nil
|
||||
}
|
||||
|
||||
end = bytes.IndexByte(in, '\r')
|
||||
if end != -1 {
|
||||
in = in[:end]
|
||||
}
|
||||
|
||||
in = bytes.TrimSpace(in)
|
||||
if len(in) == 0 || in[0] == '#' {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
i := bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
if out, comment, ok = parseAuthorizedKey(in[i:]); ok {
|
||||
return
|
||||
}
|
||||
|
||||
// No key type recognised. Maybe there's an options field at
|
||||
// the beginning.
|
||||
var b byte
|
||||
inQuote := false
|
||||
var candidateOptions []string
|
||||
optionStart := 0
|
||||
for i, b = range in {
|
||||
isEnd := !inQuote && (b == ' ' || b == '\t')
|
||||
if (b == ',' && !inQuote) || isEnd {
|
||||
if i-optionStart > 0 {
|
||||
candidateOptions = append(candidateOptions, string(in[optionStart:i]))
|
||||
}
|
||||
optionStart = i + 1
|
||||
}
|
||||
if isEnd {
|
||||
break
|
||||
}
|
||||
if b == '"' && (i == 0 || (i > 0 && in[i-1] != '\\')) {
|
||||
inQuote = !inQuote
|
||||
}
|
||||
}
|
||||
for i < len(in) && (in[i] == ' ' || in[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
if i == len(in) {
|
||||
// Invalid line: unmatched quote
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
in = in[i:]
|
||||
i = bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
if out, comment, ok = parseAuthorizedKey(in[i:]); ok {
|
||||
options = candidateOptions
|
||||
return
|
||||
}
|
||||
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ParsePublicKey parses an SSH public key formatted for use in
|
||||
// the SSH wire protocol according to RFC 4253, section 6.6.
|
||||
func ParsePublicKey(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
algo, in, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
return parsePubKey(in, string(algo))
|
||||
}
|
||||
|
||||
// MarshalAuthorizedKey returns a byte stream suitable for inclusion
|
||||
// in an OpenSSH authorized_keys file following the format specified
|
||||
// in the sshd(8) manual page.
|
||||
func MarshalAuthorizedKey(key PublicKey) []byte {
|
||||
b := &bytes.Buffer{}
|
||||
b.WriteString(key.PublicKeyAlgo())
|
||||
b.WriteByte(' ')
|
||||
e := base64.NewEncoder(base64.StdEncoding, b)
|
||||
e.Write(MarshalPublicKey(key))
|
||||
e.Close()
|
||||
b.WriteByte('\n')
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// PublicKey is an abstraction of different types of public keys.
|
||||
type PublicKey interface {
|
||||
// PrivateKeyAlgo returns the name of the encryption system.
|
||||
PrivateKeyAlgo() string
|
||||
|
||||
// PublicKeyAlgo returns the algorithm for the public key,
|
||||
// which may be different from PrivateKeyAlgo for certificates.
|
||||
PublicKeyAlgo() string
|
||||
|
||||
// Marshal returns the serialized key data in SSH wire format,
|
||||
// without the name prefix. Callers should typically use
|
||||
// MarshalPublicKey().
|
||||
Marshal() []byte
|
||||
|
||||
// Verify that sig is a signature on the given data using this
|
||||
// key. This function will hash the data appropriately first.
|
||||
Verify(data []byte, sigBlob []byte) bool
|
||||
}
|
||||
|
||||
// A Signer is can create signatures that verify against a public key.
|
||||
type Signer interface {
|
||||
// PublicKey returns an associated PublicKey instance.
|
||||
PublicKey() PublicKey
|
||||
|
||||
// Sign returns raw signature for the given data. This method
|
||||
// will apply the hash specified for the keytype to the data.
|
||||
Sign(rand io.Reader, data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
type rsaPublicKey rsa.PublicKey
|
||||
|
||||
func (r *rsaPublicKey) PrivateKeyAlgo() string {
|
||||
return "ssh-rsa"
|
||||
}
|
||||
|
||||
func (r *rsaPublicKey) PublicKeyAlgo() string {
|
||||
return r.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
// parseRSA parses an RSA key according to RFC 4253, section 6.6.
|
||||
func parseRSA(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
key := new(rsa.PublicKey)
|
||||
|
||||
bigE, in, ok := parseInt(in)
|
||||
if !ok || bigE.BitLen() > 24 {
|
||||
return
|
||||
}
|
||||
e := bigE.Int64()
|
||||
if e < 3 || e&1 == 0 {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
key.E = int(e)
|
||||
|
||||
if key.N, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return (*rsaPublicKey)(key), in, ok
|
||||
}
|
||||
|
||||
func (r *rsaPublicKey) Marshal() []byte {
|
||||
// See RFC 4253, section 6.6.
|
||||
e := new(big.Int).SetInt64(int64(r.E))
|
||||
length := intLength(e)
|
||||
length += intLength(r.N)
|
||||
|
||||
ret := make([]byte, length)
|
||||
rest := marshalInt(ret, e)
|
||||
marshalInt(rest, r.N)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *rsaPublicKey) Verify(data []byte, sig []byte) bool {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
return rsa.VerifyPKCS1v15((*rsa.PublicKey)(r), crypto.SHA1, digest, sig) == nil
|
||||
}
|
||||
|
||||
type rsaPrivateKey struct {
|
||||
*rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (r *rsaPrivateKey) PublicKey() PublicKey {
|
||||
return (*rsaPublicKey)(&r.PrivateKey.PublicKey)
|
||||
}
|
||||
|
||||
func (r *rsaPrivateKey) Sign(rand io.Reader, data []byte) ([]byte, error) {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
return rsa.SignPKCS1v15(rand, r.PrivateKey, crypto.SHA1, digest)
|
||||
}
|
||||
|
||||
type dsaPublicKey dsa.PublicKey
|
||||
|
||||
func (r *dsaPublicKey) PrivateKeyAlgo() string {
|
||||
return "ssh-dss"
|
||||
}
|
||||
|
||||
func (r *dsaPublicKey) PublicKeyAlgo() string {
|
||||
return r.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
// parseDSA parses an DSA key according to RFC 4253, section 6.6.
|
||||
func parseDSA(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
key := new(dsa.PublicKey)
|
||||
|
||||
if key.P, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if key.Q, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if key.G, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if key.Y, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return (*dsaPublicKey)(key), in, ok
|
||||
}
|
||||
|
||||
func (r *dsaPublicKey) Marshal() []byte {
|
||||
// See RFC 4253, section 6.6.
|
||||
length := intLength(r.P)
|
||||
length += intLength(r.Q)
|
||||
length += intLength(r.G)
|
||||
length += intLength(r.Y)
|
||||
|
||||
ret := make([]byte, length)
|
||||
rest := marshalInt(ret, r.P)
|
||||
rest = marshalInt(rest, r.Q)
|
||||
rest = marshalInt(rest, r.G)
|
||||
marshalInt(rest, r.Y)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (k *dsaPublicKey) Verify(data []byte, sigBlob []byte) bool {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
|
||||
// Per RFC 4253, section 6.6,
|
||||
// The value for 'dss_signature_blob' is encoded as a string containing
|
||||
// r, followed by s (which are 160-bit integers, without lengths or
|
||||
// padding, unsigned, and in network byte order).
|
||||
// For DSS purposes, sig.Blob should be exactly 40 bytes in length.
|
||||
if len(sigBlob) != 40 {
|
||||
return false
|
||||
}
|
||||
r := new(big.Int).SetBytes(sigBlob[:20])
|
||||
s := new(big.Int).SetBytes(sigBlob[20:])
|
||||
return dsa.Verify((*dsa.PublicKey)(k), digest, r, s)
|
||||
}
|
||||
|
||||
type dsaPrivateKey struct {
|
||||
*dsa.PrivateKey
|
||||
}
|
||||
|
||||
func (k *dsaPrivateKey) PublicKey() PublicKey {
|
||||
return (*dsaPublicKey)(&k.PrivateKey.PublicKey)
|
||||
}
|
||||
|
||||
func (k *dsaPrivateKey) Sign(rand io.Reader, data []byte) ([]byte, error) {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
r, s, err := dsa.Sign(rand, k.PrivateKey, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig := make([]byte, 40)
|
||||
copy(sig[:20], r.Bytes())
|
||||
copy(sig[20:], s.Bytes())
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
type ecdsaPublicKey ecdsa.PublicKey
|
||||
|
||||
func (key *ecdsaPublicKey) PrivateKeyAlgo() string {
|
||||
return "ecdsa-sha2-" + key.nistID()
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) nistID() string {
|
||||
switch key.Params().BitSize {
|
||||
case 256:
|
||||
return "nistp256"
|
||||
case 384:
|
||||
return "nistp384"
|
||||
case 521:
|
||||
return "nistp521"
|
||||
}
|
||||
panic("ssh: unsupported ecdsa key size")
|
||||
}
|
||||
|
||||
func supportedEllipticCurve(curve elliptic.Curve) bool {
|
||||
return (curve == elliptic.P256() || curve == elliptic.P384() || curve == elliptic.P521())
|
||||
}
|
||||
|
||||
// ecHash returns the hash to match the given elliptic curve, see RFC
|
||||
// 5656, section 6.2.1
|
||||
func ecHash(curve elliptic.Curve) crypto.Hash {
|
||||
bitSize := curve.Params().BitSize
|
||||
switch {
|
||||
case bitSize <= 256:
|
||||
return crypto.SHA256
|
||||
case bitSize <= 384:
|
||||
return crypto.SHA384
|
||||
}
|
||||
return crypto.SHA512
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) PublicKeyAlgo() string {
|
||||
return key.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
|
||||
func parseECDSA(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
var identifier []byte
|
||||
if identifier, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
key := new(ecdsa.PublicKey)
|
||||
|
||||
switch string(identifier) {
|
||||
case "nistp256":
|
||||
key.Curve = elliptic.P256()
|
||||
case "nistp384":
|
||||
key.Curve = elliptic.P384()
|
||||
case "nistp521":
|
||||
key.Curve = elliptic.P521()
|
||||
default:
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
var keyBytes []byte
|
||||
if keyBytes, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
key.X, key.Y = elliptic.Unmarshal(key.Curve, keyBytes)
|
||||
if key.X == nil || key.Y == nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
return (*ecdsaPublicKey)(key), in, ok
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) Marshal() []byte {
|
||||
// See RFC 5656, section 3.1.
|
||||
keyBytes := elliptic.Marshal(key.Curve, key.X, key.Y)
|
||||
|
||||
ID := key.nistID()
|
||||
length := stringLength(len(ID))
|
||||
length += stringLength(len(keyBytes))
|
||||
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, []byte(ID))
|
||||
r = marshalString(r, keyBytes)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) Verify(data []byte, sigBlob []byte) bool {
|
||||
h := ecHash(key.Curve).New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
|
||||
// Per RFC 5656, section 3.1.2,
|
||||
// The ecdsa_signature_blob value has the following specific encoding:
|
||||
// mpint r
|
||||
// mpint s
|
||||
r, rest, ok := parseInt(sigBlob)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
s, rest, ok := parseInt(rest)
|
||||
if !ok || len(rest) > 0 {
|
||||
return false
|
||||
}
|
||||
return ecdsa.Verify((*ecdsa.PublicKey)(key), digest, r, s)
|
||||
}
|
||||
|
||||
type ecdsaPrivateKey struct {
|
||||
*ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
func (k *ecdsaPrivateKey) PublicKey() PublicKey {
|
||||
return (*ecdsaPublicKey)(&k.PrivateKey.PublicKey)
|
||||
}
|
||||
|
||||
func (k *ecdsaPrivateKey) Sign(rand io.Reader, data []byte) ([]byte, error) {
|
||||
h := ecHash(k.PrivateKey.PublicKey.Curve).New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
r, s, err := ecdsa.Sign(rand, k.PrivateKey, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig := make([]byte, intLength(r)+intLength(s))
|
||||
rest := marshalInt(sig, r)
|
||||
marshalInt(rest, s)
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// NewPrivateKey takes a pointer to rsa, dsa or ecdsa PrivateKey
|
||||
// returns a corresponding Signer instance. EC keys should use P256,
|
||||
// P384 or P521.
|
||||
func NewSignerFromKey(k interface{}) (Signer, error) {
|
||||
var sshKey Signer
|
||||
switch t := k.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
sshKey = &rsaPrivateKey{t}
|
||||
case *dsa.PrivateKey:
|
||||
sshKey = &dsaPrivateKey{t}
|
||||
case *ecdsa.PrivateKey:
|
||||
if !supportedEllipticCurve(t.Curve) {
|
||||
return nil, errors.New("ssh: only P256, P384 and P521 EC keys are supported.")
|
||||
}
|
||||
|
||||
sshKey = &ecdsaPrivateKey{t}
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
|
||||
}
|
||||
return sshKey, nil
|
||||
}
|
||||
|
||||
// NewPublicKey takes a pointer to rsa, dsa or ecdsa PublicKey
|
||||
// and returns a corresponding ssh PublicKey instance. EC keys should use P256, P384 or P521.
|
||||
func NewPublicKey(k interface{}) (PublicKey, error) {
|
||||
|
||||
var sshKey PublicKey
|
||||
|
||||
switch t := k.(type) {
|
||||
case *rsa.PublicKey:
|
||||
sshKey = (*rsaPublicKey)(t)
|
||||
case *ecdsa.PublicKey:
|
||||
if !supportedEllipticCurve(t.Curve) {
|
||||
return nil, errors.New("ssh: only P256, P384 and P521 EC keys are supported.")
|
||||
}
|
||||
sshKey = (*ecdsaPublicKey)(t)
|
||||
case *dsa.PublicKey:
|
||||
sshKey = (*dsaPublicKey)(t)
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
|
||||
}
|
||||
return sshKey, nil
|
||||
}
|
||||
|
||||
// ParsePublicKey parses a PEM encoded private key. It supports
|
||||
// PKCS#1, RSA, DSA and ECDSA private keys.
|
||||
func ParsePrivateKey(pemBytes []byte) (Signer, error) {
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
return nil, errors.New("ssh: no key found")
|
||||
}
|
||||
|
||||
var rawkey interface{}
|
||||
switch block.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
rsa, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawkey = rsa
|
||||
case "EC PRIVATE KEY":
|
||||
ec, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawkey = ec
|
||||
case "DSA PRIVATE KEY":
|
||||
ec, err := parseDSAPrivate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawkey = ec
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type)
|
||||
}
|
||||
|
||||
return NewSignerFromKey(rawkey)
|
||||
}
|
||||
|
||||
// parseDSAPrivate parses a DSA key in ASN.1 DER encoding, as
|
||||
// documented in the OpenSSL DSA manpage.
|
||||
// TODO(hanwen): move this in to crypto/x509 after the Go 1.2 freeze.
|
||||
func parseDSAPrivate(p []byte) (*dsa.PrivateKey, error) {
|
||||
k := struct {
|
||||
Version int
|
||||
P *big.Int
|
||||
Q *big.Int
|
||||
G *big.Int
|
||||
Priv *big.Int
|
||||
Pub *big.Int
|
||||
}{}
|
||||
rest, err := asn1.Unmarshal(p, &k)
|
||||
if err != nil {
|
||||
return nil, errors.New("ssh: failed to parse DSA key: " + err.Error())
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, errors.New("ssh: garbage after DSA key")
|
||||
}
|
||||
|
||||
return &dsa.PrivateKey{
|
||||
PublicKey: dsa.PublicKey{
|
||||
Parameters: dsa.Parameters{
|
||||
P: k.P,
|
||||
Q: k.Q,
|
||||
G: k.G,
|
||||
},
|
||||
Y: k.Priv,
|
||||
},
|
||||
X: k.Pub,
|
||||
}, nil
|
||||
}
|
||||
187
api/crypto/private.go
Normal file
187
api/crypto/private.go
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package crypto
|
||||
|
||||
// Copy only of used functions from message.go, common.go
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const (
|
||||
kexAlgoDH1SHA1 = "diffie-hellman-group1-sha1"
|
||||
kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
|
||||
kexAlgoECDH256 = "ecdh-sha2-nistp256"
|
||||
kexAlgoECDH384 = "ecdh-sha2-nistp384"
|
||||
kexAlgoECDH521 = "ecdh-sha2-nistp521"
|
||||
)
|
||||
|
||||
var bigOne = big.NewInt(1)
|
||||
|
||||
func stringLength(n int) int {
|
||||
return 4 + n
|
||||
}
|
||||
|
||||
// MarshalPublicKey serializes a supported key or certificate for use
|
||||
// by the SSH wire protocol. It can be used for comparison with the
|
||||
// pubkey argument of ServerConfig's PublicKeyCallback as well as for
|
||||
// generating an authorized_keys or host_keys file.
|
||||
func MarshalPublicKey(key PublicKey) []byte {
|
||||
// See also RFC 4253 6.6.
|
||||
algoname := key.PublicKeyAlgo()
|
||||
blob := key.Marshal()
|
||||
|
||||
length := stringLength(len(algoname))
|
||||
length += len(blob)
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, []byte(algoname))
|
||||
copy(r, blob)
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseInt(in []byte) (out *big.Int, rest []byte, ok bool) {
|
||||
contents, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
out = new(big.Int)
|
||||
|
||||
if len(contents) > 0 && contents[0]&0x80 == 0x80 {
|
||||
// This is a negative number
|
||||
notBytes := make([]byte, len(contents))
|
||||
for i := range notBytes {
|
||||
notBytes[i] = ^contents[i]
|
||||
}
|
||||
out.SetBytes(notBytes)
|
||||
out.Add(out, bigOne)
|
||||
out.Neg(out)
|
||||
} else {
|
||||
// Positive number
|
||||
out.SetBytes(contents)
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func parseUint32(in []byte) (uint32, []byte, bool) {
|
||||
if len(in) < 4 {
|
||||
return 0, nil, false
|
||||
}
|
||||
return binary.BigEndian.Uint32(in), in[4:], true
|
||||
}
|
||||
|
||||
func parseUint64(in []byte) (uint64, []byte, bool) {
|
||||
if len(in) < 8 {
|
||||
return 0, nil, false
|
||||
}
|
||||
return binary.BigEndian.Uint64(in), in[8:], true
|
||||
}
|
||||
|
||||
func parseString(in []byte) (out, rest []byte, ok bool) {
|
||||
if len(in) < 4 {
|
||||
return
|
||||
}
|
||||
length := binary.BigEndian.Uint32(in)
|
||||
if uint32(len(in)) < 4+length {
|
||||
return
|
||||
}
|
||||
out = in[4 : 4+length]
|
||||
rest = in[4+length:]
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func intLength(n *big.Int) int {
|
||||
length := 4 /* length bytes */
|
||||
if n.Sign() < 0 {
|
||||
nMinus1 := new(big.Int).Neg(n)
|
||||
nMinus1.Sub(nMinus1, bigOne)
|
||||
bitLen := nMinus1.BitLen()
|
||||
if bitLen%8 == 0 {
|
||||
// The number will need 0xff padding
|
||||
length++
|
||||
}
|
||||
length += (bitLen + 7) / 8
|
||||
} else if n.Sign() == 0 {
|
||||
// A zero is the zero length string
|
||||
} else {
|
||||
bitLen := n.BitLen()
|
||||
if bitLen%8 == 0 {
|
||||
// The number will need 0x00 padding
|
||||
length++
|
||||
}
|
||||
length += (bitLen + 7) / 8
|
||||
}
|
||||
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalUint32(to []byte, n uint32) []byte {
|
||||
binary.BigEndian.PutUint32(to, n)
|
||||
return to[4:]
|
||||
}
|
||||
|
||||
func marshalUint64(to []byte, n uint64) []byte {
|
||||
binary.BigEndian.PutUint64(to, n)
|
||||
return to[8:]
|
||||
}
|
||||
|
||||
func marshalInt(to []byte, n *big.Int) []byte {
|
||||
lengthBytes := to
|
||||
to = to[4:]
|
||||
length := 0
|
||||
|
||||
if n.Sign() < 0 {
|
||||
// A negative number has to be converted to two's-complement
|
||||
// form. So we'll subtract 1 and invert. If the
|
||||
// most-significant-bit isn't set then we'll need to pad the
|
||||
// beginning with 0xff in order to keep the number negative.
|
||||
nMinus1 := new(big.Int).Neg(n)
|
||||
nMinus1.Sub(nMinus1, bigOne)
|
||||
bytes := nMinus1.Bytes()
|
||||
for i := range bytes {
|
||||
bytes[i] ^= 0xff
|
||||
}
|
||||
if len(bytes) == 0 || bytes[0]&0x80 == 0 {
|
||||
to[0] = 0xff
|
||||
to = to[1:]
|
||||
length++
|
||||
}
|
||||
nBytes := copy(to, bytes)
|
||||
to = to[nBytes:]
|
||||
length += nBytes
|
||||
} else if n.Sign() == 0 {
|
||||
// A zero is the zero length string
|
||||
} else {
|
||||
bytes := n.Bytes()
|
||||
if len(bytes) > 0 && bytes[0]&0x80 != 0 {
|
||||
// We'll have to pad this with a 0x00 in order to
|
||||
// stop it looking like a negative number.
|
||||
to[0] = 0
|
||||
to = to[1:]
|
||||
length++
|
||||
}
|
||||
nBytes := copy(to, bytes)
|
||||
to = to[nBytes:]
|
||||
length += nBytes
|
||||
}
|
||||
|
||||
lengthBytes[0] = byte(length >> 24)
|
||||
lengthBytes[1] = byte(length >> 16)
|
||||
lengthBytes[2] = byte(length >> 8)
|
||||
lengthBytes[3] = byte(length)
|
||||
return to
|
||||
}
|
||||
|
||||
func marshalString(to []byte, s []byte) []byte {
|
||||
to[0] = byte(len(s) >> 24)
|
||||
to[1] = byte(len(s) >> 16)
|
||||
to[2] = byte(len(s) >> 8)
|
||||
to[3] = byte(len(s))
|
||||
to = to[4:]
|
||||
copy(to, s)
|
||||
return to[len(s):]
|
||||
}
|
||||
@@ -71,6 +71,11 @@ const (
|
||||
ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint")
|
||||
)
|
||||
|
||||
// Deploykey errors
|
||||
const (
|
||||
ErrDeploykeyAlreadyExists = Error("A key already exists with this name")
|
||||
)
|
||||
|
||||
// Crypto errors.
|
||||
const (
|
||||
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
const (
|
||||
errInvalidResponseStatus = portainer.Error("Invalid response status (expecting 200)")
|
||||
defaultHTTPTimeout = 5
|
||||
)
|
||||
|
||||
// HTTPClient represents a client to send HTTP requests.
|
||||
@@ -26,7 +27,7 @@ type HTTPClient struct {
|
||||
func NewHTTPClient() *HTTPClient {
|
||||
return &HTTPClient{
|
||||
&http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
Timeout: time.Second * time.Duration(defaultHTTPTimeout),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -67,10 +68,16 @@ func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portain
|
||||
}
|
||||
|
||||
// Get executes a simple HTTP GET to the specified URL and returns
|
||||
// the content of the response body.
|
||||
func Get(url string) ([]byte, error) {
|
||||
// the content of the response body. Timeout can be specified via the timeout parameter,
|
||||
// will default to defaultHTTPTimeout if set to 0.
|
||||
func Get(url string, timeout int) ([]byte, error) {
|
||||
|
||||
if timeout == 0 {
|
||||
timeout = defaultHTTPTimeout
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 3,
|
||||
Timeout: time.Second * time.Duration(timeout),
|
||||
}
|
||||
|
||||
response, err := client.Get(url)
|
||||
|
||||
76
api/http/handler/deploykeys/deploykey_create.go
Normal file
76
api/http/handler/deploykeys/deploykey_create.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type deploykeyCreatePayload struct {
|
||||
Name string
|
||||
Privatekeypath string
|
||||
Publickeypath string
|
||||
UserID int
|
||||
LastUsage string
|
||||
}
|
||||
|
||||
func (payload *deploykeyCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid deploykey Name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/deploykeys
|
||||
func (handler *Handler) deploykeyCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload deploykeyCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
deploykeys, err := handler.DeploykeyService.Deploykeys()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve deploykeys from the database", err}
|
||||
}
|
||||
|
||||
for _, deploykey := range deploykeys {
|
||||
if deploykey.Name == payload.Name {
|
||||
return &httperror.HandlerError{http.StatusConflict, "This name is already associated to a deploykey", portainer.ErrDeploykeyAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
pubkeypath, errrrr := handler.DigitalDeploykeyService.GenerateSshKey()
|
||||
|
||||
if errrrr != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid key payload", errrrr}
|
||||
}
|
||||
|
||||
//encodedStr := hex.EncodeToString(privatepath)
|
||||
encodedStr1 := hex.EncodeToString(pubkeypath)
|
||||
dateTime := time.Now().Local().Format("2006-01-02 15:04:05")
|
||||
|
||||
deploykey := &portainer.Deploykey{
|
||||
Name: payload.Name,
|
||||
Privatekeypath: "abc",
|
||||
Publickeypath: encodedStr1,
|
||||
UserID: payload.UserID,
|
||||
LastUsage: dateTime,
|
||||
}
|
||||
|
||||
err = handler.DeploykeyService.CreateDeploykey(deploykey)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the deploykey inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, deploykey)
|
||||
}
|
||||
func BytesToString(data []byte) string {
|
||||
return string(data[:])
|
||||
}
|
||||
25
api/http/handler/deploykeys/deploykey_delete.go
Normal file
25
api/http/handler/deploykeys/deploykey_delete.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// DELETE request on /api/deploykeys/:id
|
||||
func (handler *Handler) deploykeyDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid deploykey identifier route variable", err}
|
||||
}
|
||||
|
||||
err = handler.DeploykeyService.DeleteDeploykey(portainer.DeploykeyID(id))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the deploykey from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
18
api/http/handler/deploykeys/deploykey_list.go
Normal file
18
api/http/handler/deploykeys/deploykey_list.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// GET request on /api/deploykeys
|
||||
func (handler *Handler) deploykeyList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
deploykeys, err := handler.DeploykeyService.Deploykeys()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve deploykeys from the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, deploykeys)
|
||||
}
|
||||
35
api/http/handler/deploykeys/handler.go
Normal file
35
api/http/handler/deploykeys/handler.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle deploykey operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DeploykeyService portainer.DeploykeyService
|
||||
CryptoService portainer.CryptoService
|
||||
DigitalDeploykeyService portainer.DigitalDeploykeyService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage deploykey operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/deploykeys",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploykeyCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/deploykeys",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploykeyList))).Methods(http.MethodGet)
|
||||
h.Handle("/deploykeys/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploykeyDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -34,7 +34,6 @@ func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
}
|
||||
|
||||
w.Header().Add("X-Frame-Options", "DENY")
|
||||
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
handler.Handler.ServeHTTP(w, r)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/http/handler/auth"
|
||||
"github.com/portainer/portainer/http/handler/deploykeys"
|
||||
"github.com/portainer/portainer/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/http/handler/endpointproxy"
|
||||
@@ -49,6 +50,7 @@ type Handler struct {
|
||||
UserHandler *users.Handler
|
||||
WebSocketHandler *websocket.Handler
|
||||
WebhookHandler *webhooks.Handler
|
||||
DeploykeyHandler *deploykeys.Handler
|
||||
}
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
@@ -87,6 +89,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/deploykeys"):
|
||||
http.StripPrefix("/api", h.DeploykeyHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/upload"):
|
||||
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/users"):
|
||||
|
||||
@@ -16,7 +16,7 @@ type motdResponse struct {
|
||||
|
||||
func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
motd, err := client.Get(portainer.MessageOfTheDayURL)
|
||||
motd, err := client.Get(portainer.MessageOfTheDayURL, 0)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
type stackMigratePayload struct {
|
||||
EndpointID int
|
||||
SwarmID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (payload *stackMigratePayload) Validate(r *http.Request) error {
|
||||
@@ -89,11 +90,17 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||
stack.SwarmID = payload.SwarmID
|
||||
}
|
||||
|
||||
oldName := stack.Name
|
||||
if payload.Name != "" {
|
||||
stack.Name = payload.Name
|
||||
}
|
||||
|
||||
migrationError := handler.migrateStack(r, stack, targetEndpoint)
|
||||
if migrationError != nil {
|
||||
return migrationError
|
||||
}
|
||||
|
||||
stack.Name = oldName
|
||||
err = handler.deleteStack(stack, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
|
||||
@@ -26,7 +26,7 @@ func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *ht
|
||||
}
|
||||
} else {
|
||||
var templateData []byte
|
||||
templateData, err = client.Get(settings.TemplatesURL)
|
||||
templateData, err = client.Get(settings.TemplatesURL, 0)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,6 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain
|
||||
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("X-Frame-Options", "DENY")
|
||||
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/portainer/portainer/docker"
|
||||
"github.com/portainer/portainer/http/handler"
|
||||
"github.com/portainer/portainer/http/handler/auth"
|
||||
"github.com/portainer/portainer/http/handler/deploykeys"
|
||||
"github.com/portainer/portainer/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/http/handler/endpointproxy"
|
||||
@@ -48,6 +49,7 @@ type Server struct {
|
||||
DockerHubService portainer.DockerHubService
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
DigitalDeploykeyService portainer.DigitalDeploykeyService
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
JWTService portainer.JWTService
|
||||
@@ -62,6 +64,7 @@ type Server struct {
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
TemplateService portainer.TemplateService
|
||||
UserService portainer.UserService
|
||||
DeploykeyService portainer.DeploykeyService
|
||||
WebhookService portainer.WebhookService
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
@@ -152,6 +155,10 @@ func (server *Server) Start() error {
|
||||
teamHandler.TeamService = server.TeamService
|
||||
teamHandler.TeamMembershipService = server.TeamMembershipService
|
||||
|
||||
var deploykeyHandler = deploykeys.NewHandler(requestBouncer)
|
||||
deploykeyHandler.DeploykeyService = server.DeploykeyService
|
||||
deploykeyHandler.DigitalDeploykeyService = server.DigitalDeploykeyService
|
||||
|
||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
||||
@@ -201,6 +208,7 @@ func (server *Server) Start() error {
|
||||
UserHandler: userHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
WebhookHandler: webhookHandler,
|
||||
DeploykeyHandler: deploykeyHandler,
|
||||
}
|
||||
|
||||
if server.SSL {
|
||||
|
||||
@@ -22,11 +22,13 @@ type Service struct{}
|
||||
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
|
||||
var userDN string
|
||||
found := false
|
||||
usernameEscaped := ldap.EscapeFilter(username)
|
||||
|
||||
for _, searchSettings := range settings {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
searchSettings.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, username),
|
||||
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, usernameEscaped),
|
||||
[]string{"dn"},
|
||||
nil,
|
||||
)
|
||||
@@ -134,12 +136,13 @@ func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings)
|
||||
// Get a list of group names for specified user from LDAP/AD
|
||||
func getGroups(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
|
||||
groups := make([]string, 0)
|
||||
userDNEscaped := ldap.EscapeFilter(userDN)
|
||||
|
||||
for _, searchSettings := range settings {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
searchSettings.GroupBaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&%s(%s=%s))", searchSettings.GroupFilter, searchSettings.GroupAttribute, userDN),
|
||||
fmt.Sprintf("(&%s(%s=%s))", searchSettings.GroupFilter, searchSettings.GroupAttribute, userDNEscaped),
|
||||
[]string{"cn"},
|
||||
nil,
|
||||
)
|
||||
|
||||
@@ -321,6 +321,19 @@ type (
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
|
||||
}
|
||||
|
||||
// DeploykeyID represents a key identifier
|
||||
DeploykeyID int
|
||||
|
||||
// Deploykey represents a key that can be associated to a resource
|
||||
Deploykey struct {
|
||||
ID DeploykeyID
|
||||
Name string `json:"Name"`
|
||||
Privatekeypath string `json:"Privatekeypath"`
|
||||
Publickeypath string `json:"Publickeypath"`
|
||||
UserID int `json:"UserID"`
|
||||
LastUsage string `json:"LastUsage"`
|
||||
}
|
||||
|
||||
// TagID represents a tag identifier
|
||||
TagID int
|
||||
|
||||
@@ -450,6 +463,13 @@ type (
|
||||
DeleteTeam(ID TeamID) error
|
||||
}
|
||||
|
||||
// DeploykeyService represents a service for managing key data
|
||||
DeploykeyService interface {
|
||||
Deploykeys() ([]Deploykey, error)
|
||||
CreateDeploykey(deploykey *Deploykey) error
|
||||
DeleteDeploykey(ID DeploykeyID) error
|
||||
}
|
||||
|
||||
// TeamMembershipService represents a service for managing team membership data
|
||||
TeamMembershipService interface {
|
||||
TeamMembership(ID TeamMembershipID) (*TeamMembership, error)
|
||||
@@ -572,6 +592,16 @@ type (
|
||||
Sign(message string) (string, error)
|
||||
}
|
||||
|
||||
//DigitalDeploykeyService represents a service to manage digital deploykey
|
||||
DigitalDeploykeyService interface {
|
||||
ParseKeyPair(private, public []byte) error
|
||||
GenerateKeyPair() ([]byte, []byte, error)
|
||||
GenerateSshKey()([]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)
|
||||
@@ -639,7 +669,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "1.19.2"
|
||||
APIVersion = "1.20-dev"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 14
|
||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||
|
||||
171
api/swagger.yaml
171
api/swagger.yaml
@@ -54,13 +54,15 @@ info:
|
||||
|
||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||
|
||||
version: "1.19.2"
|
||||
version: "1.20-dev"
|
||||
title: "Portainer API"
|
||||
contact:
|
||||
email: "info@portainer.io"
|
||||
host: "portainer.domain"
|
||||
basePath: "/api"
|
||||
tags:
|
||||
- name: "deploykeys"
|
||||
description: "Manage deploykeys"
|
||||
- name: "auth"
|
||||
description: "Authenticate against Portainer HTTP API"
|
||||
- name: "dockerhub"
|
||||
@@ -2311,6 +2313,105 @@ paths:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/deploykeys:
|
||||
get:
|
||||
tags:
|
||||
- "deploykeys"
|
||||
summary: "List deploykeys"
|
||||
description: |
|
||||
List deploykeys.
|
||||
**Access policy**: administrator
|
||||
operationId: "DeploykeysList"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/DeploykeyListResponse"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
404:
|
||||
description: "Key not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
post:
|
||||
tags:
|
||||
- "deploykeys"
|
||||
summary: "Create a new deploykeys"
|
||||
description: |
|
||||
Create a new deploykeys.
|
||||
**Access policy**: administrator
|
||||
operationId: "DeploykeyCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Deploykey details"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/DeploykeyCreateRequest"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/Deploykey"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
409:
|
||||
description: "Conflict"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "A deploykeys with the specified name already exists"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/deploykeys/{id}:
|
||||
delete:
|
||||
tags:
|
||||
- "deploykeys"
|
||||
summary: "Remove a deploykeys"
|
||||
description: |
|
||||
Remove a deploykeys.
|
||||
**Access policy**: administrator
|
||||
operationId: "DeploykeyDelete"
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Deploykey identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
responses:
|
||||
204:
|
||||
description: "Success"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/teams/{id}/memberships:
|
||||
get:
|
||||
@@ -2735,6 +2836,35 @@ definitions:
|
||||
type: "string"
|
||||
example: "org/acme"
|
||||
description: "Tag name"
|
||||
|
||||
Deploykey:
|
||||
type: "object"
|
||||
properties:
|
||||
Id:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Deploykey identifier"
|
||||
Name:
|
||||
type: "string"
|
||||
example: "abcd"
|
||||
description: "Deploykey name"
|
||||
Privatekeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey private key path"
|
||||
Publickeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey public key path"
|
||||
UserID:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Deploykey user id"
|
||||
LastUsage:
|
||||
type: "string"
|
||||
example: "12:03:32"
|
||||
description: " last usage deploykey date"
|
||||
|
||||
Team:
|
||||
type: "object"
|
||||
properties:
|
||||
@@ -2816,7 +2946,7 @@ definitions:
|
||||
description: "Is analytics enabled"
|
||||
Version:
|
||||
type: "string"
|
||||
example: "1.19.2"
|
||||
example: "1.20-dev"
|
||||
description: "Portainer API version"
|
||||
PublicSettingsInspectResponse:
|
||||
type: "object"
|
||||
@@ -3717,6 +3847,37 @@ definitions:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/TeamMembership"
|
||||
DeploykeyListResponse:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/Deploykey"
|
||||
|
||||
DeploykeyCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "Name"
|
||||
properties:
|
||||
Name:
|
||||
type: "string"
|
||||
example: "abcd"
|
||||
description: "Deploykey name"
|
||||
Privatekeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey private key path"
|
||||
Publickeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey public key path"
|
||||
UserID:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Deploykey user id"
|
||||
LastUsage:
|
||||
type: "string"
|
||||
example: "12:03:32"
|
||||
description: "last usage deploykey date"
|
||||
|
||||
|
||||
TeamMembershipCreateRequest:
|
||||
type: "object"
|
||||
@@ -4160,6 +4321,10 @@ definitions:
|
||||
type: "string"
|
||||
example: "jpofkc0i9uo9wtx1zesuk649w"
|
||||
description: "Swarm cluster identifier, must match the identifier of the cluster where the stack will be relocated"
|
||||
Name:
|
||||
type: "string"
|
||||
example: "new-stack"
|
||||
description: "If provided will rename the migrated stack"
|
||||
StackCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
@@ -4269,7 +4434,7 @@ definitions:
|
||||
Prune:
|
||||
type: "boolean"
|
||||
example: false
|
||||
description: "Prune services that are no longer referenced"
|
||||
description: "Prune services that are no longer referenced (only available for Swarm stacks)"
|
||||
StackFileInspectResponse:
|
||||
type: "object"
|
||||
properties:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
angular.module('portainer.agent').controller('FileUploaderController', [
|
||||
'$q',
|
||||
function FileUploaderController($q) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.state = {
|
||||
uploadInProgress: false
|
||||
};
|
||||
|
||||
ctrl.onFileSelected = onFileSelected;
|
||||
|
||||
function onFileSelected(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctrl.state.uploadInProgress = true;
|
||||
$q.when(ctrl.uploadFile(file)).finally(function toggleProgress() {
|
||||
ctrl.state.uploadInProgress = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
6
app/agent/components/file-uploader/file-uploader.html
Normal file
6
app/agent/components/file-uploader/file-uploader.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<button
|
||||
ngf-select="$ctrl.onFileSelected($file)"
|
||||
class="btn ng-scope"
|
||||
button-spinner="$ctrl.state.uploadInProgress">
|
||||
<i style="margin:0" class="fa fa-upload" ng-if="!$ctrl.state.uploadInProgress"></i>
|
||||
</button>
|
||||
7
app/agent/components/file-uploader/file-uploader.js
Normal file
7
app/agent/components/file-uploader/file-uploader.js
Normal file
@@ -0,0 +1,7 @@
|
||||
angular.module('portainer.agent').component('fileUploader', {
|
||||
templateUrl: 'app/agent/components/file-uploader/file-uploader.html',
|
||||
controller: 'FileUploaderController',
|
||||
bindings: {
|
||||
uploadFile: '<onFileSelected'
|
||||
}
|
||||
});
|
||||
@@ -1,14 +1,14 @@
|
||||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="{{$ctrl.titleIcon}}" title-text="{{ $ctrl.titleText }}">
|
||||
<file-uploader ng-if="$ctrl.isUploadAllowed" on-file-selected="$ctrl.onFileSelectedForUpload">
|
||||
</file-uploader>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter"
|
||||
placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
@@ -41,23 +41,29 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-if="$ctrl.volumeBrowser.state.path !== '/'">
|
||||
<tr ng-if="!$ctrl.isRoot">
|
||||
<td colspan="4">
|
||||
<a ng-click="$ctrl.volumeBrowser.up()"><i class="fa fa-level-up-alt space-right"></i>Go to parent</a>
|
||||
<a ng-click="$ctrl.goToParent()"><i class="fa fa-level-up-alt space-right"></i>Go
|
||||
to parent</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))">
|
||||
<td>
|
||||
<span ng-if="item.edit">
|
||||
<input class="input-sm" type="text" ng-model="item.newName" on-enter-key="$ctrl.volumeBrowser.rename(item.Name, item.newName); item.edit = false;" auto-focus />
|
||||
<input class="input-sm" type="text" ng-model="item.newName"
|
||||
on-enter-key="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"
|
||||
auto-focus />
|
||||
<a class="interactive" ng-click="item.edit = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="$ctrl.volumeBrowser.rename(item.Name, item.newName); item.edit = false;"><i class="fa fa-check-square"></i></a>
|
||||
<a class="interactive" ng-click="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"><i
|
||||
class="fa fa-check-square"></i></a>
|
||||
</span>
|
||||
<span ng-if="!item.edit && item.Dir">
|
||||
<a ng-click="$ctrl.volumeBrowser.browse(item.Name)"><i class="fa fa-folder space-right" aria-hidden="true"></i>{{ item.Name }}</a>
|
||||
<a ng-click="$ctrl.browse({name: item.Name})"><i class="fa fa-folder space-right"
|
||||
aria-hidden="true"></i>{{ item.Name }}</a>
|
||||
</span>
|
||||
<span ng-if="!item.edit && !item.Dir">
|
||||
<i class="fa fa-file space-right" aria-hidden="true"></i>{{ item.Name }}
|
||||
<i class="fa fa-file space-right" aria-hidden="true"></i>{{
|
||||
item.Name }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ item.Size | humansize }}</td>
|
||||
@@ -65,13 +71,14 @@
|
||||
{{ item.ModTime | getisodatefromtimestamp }}
|
||||
</td>
|
||||
<td>
|
||||
<btn class="btn btn-xs btn-primary space-right" ng-click="$ctrl.volumeBrowser.download(item.Name)" ng-if="!item.Dir">
|
||||
<btn class="btn btn-xs btn-primary space-right" ng-click="$ctrl.download({ name: item.Name })"
|
||||
ng-if="!item.Dir">
|
||||
<i class="fa fa-download" aria-hidden="true"></i> Download
|
||||
</btn>
|
||||
<btn class="btn btn-xs btn-primary space-right" ng-click="item.newName = item.Name; item.edit = true">
|
||||
<i class="fa fa-edit" aria-hidden="true"></i> Rename
|
||||
</btn>
|
||||
<btn class="btn btn-xs btn-danger" ng-click="$ctrl.volumeBrowser.delete(item.Name)">
|
||||
<btn class="btn btn-xs btn-danger" ng-click="$ctrl.delete({ name: item.Name })">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i> Delete
|
||||
</btn>
|
||||
</td>
|
||||
@@ -87,4 +94,4 @@
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
22
app/agent/components/files-datatable/files-datatable.js
Normal file
22
app/agent/components/files-datatable/files-datatable.js
Normal file
@@ -0,0 +1,22 @@
|
||||
angular.module('portainer.agent').component('filesDatatable', {
|
||||
templateUrl: 'app/agent/components/files-datatable/files-datatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
|
||||
isRoot: '<',
|
||||
goToParent: '&',
|
||||
browse: '&',
|
||||
rename: '&',
|
||||
download: '&',
|
||||
delete: '&',
|
||||
|
||||
isUploadAllowed: '<',
|
||||
onFileSelectedForUpload: '<'
|
||||
}
|
||||
});
|
||||
147
app/agent/components/host-browser/host-browser-controller.js
Normal file
147
app/agent/components/host-browser/host-browser-controller.js
Normal file
@@ -0,0 +1,147 @@
|
||||
angular.module('portainer.agent').controller('HostBrowserController', [
|
||||
'HostBrowserService', 'Notifications', 'FileSaver', 'ModalService',
|
||||
function HostBrowserController(HostBrowserService, Notifications, FileSaver, ModalService) {
|
||||
var ctrl = this;
|
||||
var ROOT_PATH = '/host';
|
||||
ctrl.state = {
|
||||
path: ROOT_PATH
|
||||
};
|
||||
|
||||
ctrl.goToParent = goToParent;
|
||||
ctrl.browse = browse;
|
||||
ctrl.renameFile = renameFile;
|
||||
ctrl.downloadFile = downloadFile;
|
||||
ctrl.deleteFile = confirmDeleteFile;
|
||||
ctrl.isRoot = isRoot;
|
||||
ctrl.onFileSelectedForUpload = onFileSelectedForUpload;
|
||||
ctrl.$onInit = $onInit;
|
||||
ctrl.getRelativePath = getRelativePath;
|
||||
|
||||
function getRelativePath(path) {
|
||||
path = path || ctrl.state.path;
|
||||
var rootPathRegex = new RegExp('^' + ROOT_PATH + '\/?');
|
||||
var relativePath = path.replace(rootPathRegex, '/');
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
function goToParent() {
|
||||
getFilesForPath(parentPath(this.state.path));
|
||||
}
|
||||
|
||||
function isRoot() {
|
||||
return ctrl.state.path === ROOT_PATH;
|
||||
}
|
||||
|
||||
function browse(folder) {
|
||||
getFilesForPath(buildPath(ctrl.state.path, folder));
|
||||
}
|
||||
|
||||
function getFilesForPath(path) {
|
||||
HostBrowserService.ls(path)
|
||||
.then(function onFilesLoaded(files) {
|
||||
ctrl.state.path = path;
|
||||
ctrl.files = files;
|
||||
})
|
||||
.catch(function onLoadingFailed(err) {
|
||||
Notifications.error('Failure', err, 'Unable to browse');
|
||||
});
|
||||
}
|
||||
|
||||
function renameFile(name, newName) {
|
||||
var filePath = buildPath(ctrl.state.path, name);
|
||||
var newFilePath = buildPath(ctrl.state.path, newName);
|
||||
|
||||
HostBrowserService.rename(filePath, newFilePath)
|
||||
.then(function onRenameSuccess() {
|
||||
Notifications.success('File successfully renamed', getRelativePath(newFilePath));
|
||||
return HostBrowserService.ls(ctrl.state.path);
|
||||
})
|
||||
.then(function onFilesLoaded(files) {
|
||||
ctrl.files = files;
|
||||
})
|
||||
.catch(function notifyOnError(err) {
|
||||
Notifications.error('Failure', err, 'Unable to rename file');
|
||||
});
|
||||
}
|
||||
|
||||
function downloadFile(file) {
|
||||
var filePath = buildPath(ctrl.state.path, file);
|
||||
HostBrowserService.get(filePath)
|
||||
.then(function onFileReceived(data) {
|
||||
var downloadData = new Blob([data.file], {
|
||||
type: 'text/plain;charset=utf-8'
|
||||
});
|
||||
FileSaver.saveAs(downloadData, file);
|
||||
})
|
||||
.catch(function notifyOnError(err) {
|
||||
Notifications.error('Failure', err, 'Unable to download file');
|
||||
});
|
||||
}
|
||||
|
||||
function confirmDeleteFile(name) {
|
||||
var filePath = buildPath(ctrl.state.path, name);
|
||||
|
||||
ModalService.confirmDeletion(
|
||||
'Are you sure that you want to delete ' + getRelativePath(filePath) + ' ?',
|
||||
function onConfirm(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
return deleteFile(filePath);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deleteFile(path) {
|
||||
HostBrowserService.delete(path)
|
||||
.then(function onDeleteSuccess() {
|
||||
Notifications.success('File successfully deleted', getRelativePath(path));
|
||||
return HostBrowserService.ls(ctrl.state.path);
|
||||
})
|
||||
.then(function onFilesLoaded(data) {
|
||||
ctrl.files = data;
|
||||
})
|
||||
.catch(function notifyOnError(err) {
|
||||
Notifications.error('Failure', err, 'Unable to delete file');
|
||||
});
|
||||
}
|
||||
|
||||
function $onInit() {
|
||||
getFilesForPath(ROOT_PATH);
|
||||
}
|
||||
|
||||
function parentPath(path) {
|
||||
if (path === ROOT_PATH) {
|
||||
return ROOT_PATH;
|
||||
}
|
||||
|
||||
var split = _.split(path, '/');
|
||||
return _.join(_.slice(split, 0, split.length - 1), '/');
|
||||
}
|
||||
|
||||
function buildPath(parent, file) {
|
||||
if (parent.lastIndexOf('/') === parent.length - 1) {
|
||||
return parent + file;
|
||||
}
|
||||
return parent + '/' + file;
|
||||
}
|
||||
|
||||
function onFileSelectedForUpload(file) {
|
||||
HostBrowserService.upload(ctrl.state.path, file)
|
||||
.then(function onFileUpload() {
|
||||
onFileUploaded();
|
||||
})
|
||||
.catch(function onFileUpload(err) {
|
||||
Notifications.error('Failure', err, 'Unable to upload file');
|
||||
});
|
||||
}
|
||||
|
||||
function onFileUploaded() {
|
||||
refreshList();
|
||||
}
|
||||
|
||||
function refreshList() {
|
||||
getFilesForPath(ctrl.state.path);
|
||||
}
|
||||
}
|
||||
]);
|
||||
16
app/agent/components/host-browser/host-browser.html
Normal file
16
app/agent/components/host-browser/host-browser.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<files-datatable
|
||||
title-text="Host browser - {{$ctrl.getRelativePath()}}" title-icon="fa-file"
|
||||
dataset="$ctrl.files" table-key="host_browser"
|
||||
order-by="Dir"
|
||||
is-root="$ctrl.isRoot()"
|
||||
go-to-parent="$ctrl.goToParent()"
|
||||
browse="$ctrl.browse(name)"
|
||||
rename="$ctrl.renameFile(name, newName)"
|
||||
download="$ctrl.downloadFile(name)"
|
||||
delete="$ctrl.deleteFile(name)"
|
||||
|
||||
is-upload-allowed="true"
|
||||
on-file-selected-for-upload="$ctrl.onFileSelectedForUpload"
|
||||
>
|
||||
|
||||
</files-datatable>
|
||||
5
app/agent/components/host-browser/host-browser.js
Normal file
5
app/agent/components/host-browser/host-browser.js
Normal file
@@ -0,0 +1,5 @@
|
||||
angular.module('portainer.agent').component('hostBrowser', {
|
||||
controller: 'HostBrowserController',
|
||||
templateUrl: 'app/agent/components/host-browser/host-browser.html',
|
||||
bindings: {}
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
angular.module('portainer.agent').component('volumeBrowserDatatable', {
|
||||
templateUrl: 'app/agent/components/volume-browser/volume-browser-datatable/volumeBrowserDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<'
|
||||
},
|
||||
require: {
|
||||
volumeBrowser: '^^volumeBrowser'
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,11 @@
|
||||
<volume-browser-datatable
|
||||
<files-datatable
|
||||
title-text="Volume browser" title-icon="fa-file"
|
||||
dataset="$ctrl.files" table-key="volume_browser"
|
||||
order-by="Dir"
|
||||
></volume-browser-datatable>
|
||||
is-root="$ctrl.state.path === '/'"
|
||||
go-to-parent="$ctrl.up()"
|
||||
browse="$ctrl.browse(name)"
|
||||
rename="$ctrl.rename(name, newName)"
|
||||
download="$ctrl.download(name)"
|
||||
delete="$ctrl.delete(name)"
|
||||
></files-datatable>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
angular.module('portainer.agent')
|
||||
.factory('Browse', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:id/:action', {
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:action', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
||||
ls: {
|
||||
method: 'GET', isArray: true, params: { id: '@id', action: 'ls' }
|
||||
method: 'GET', isArray: true, params: { action: 'ls' }
|
||||
},
|
||||
get: {
|
||||
method: 'GET', params: { id: '@id', action: 'get' },
|
||||
method: 'GET', params: { action: 'get' },
|
||||
transformResponse: browseGetResponse
|
||||
},
|
||||
delete: {
|
||||
method: 'DELETE', params: { id: '@id', action: 'delete' }
|
||||
method: 'DELETE', params: { action: 'delete' }
|
||||
},
|
||||
rename: {
|
||||
method: 'PUT', params: { id: '@id', action: 'rename' }
|
||||
method: 'PUT', params: { action: 'rename' }
|
||||
}
|
||||
});
|
||||
}]);
|
||||
|
||||
15
app/agent/rest/host.js
Normal file
15
app/agent/rest/host.js
Normal file
@@ -0,0 +1,15 @@
|
||||
angular.module('portainer.agent').factory('Host', [
|
||||
'$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
|
||||
function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(
|
||||
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/host/:action',
|
||||
{
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
||||
info: { method: 'GET', params: { action: 'info' } }
|
||||
}
|
||||
);
|
||||
}
|
||||
]);
|
||||
@@ -1,24 +1,34 @@
|
||||
angular.module('portainer.agent')
|
||||
.factory('AgentService', ['$q', 'Agent', function AgentServiceFactory($q, Agent) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
angular.module('portainer.agent').factory('AgentService', [
|
||||
'$q', 'Agent','HttpRequestHelper', 'Host',
|
||||
function AgentServiceFactory($q, Agent, HttpRequestHelper, Host) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.agents = function() {
|
||||
var deferred = $q.defer();
|
||||
service.agents = agents;
|
||||
service.hostInfo = hostInfo;
|
||||
|
||||
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 });
|
||||
});
|
||||
function hostInfo(nodeName) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
return Host.info().$promise;
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
function agents() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
return service;
|
||||
}]);
|
||||
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;
|
||||
}
|
||||
]);
|
||||
|
||||
44
app/agent/services/hostBrowserService.js
Normal file
44
app/agent/services/hostBrowserService.js
Normal file
@@ -0,0 +1,44 @@
|
||||
angular.module('portainer.agent').factory('HostBrowserService', [
|
||||
'Browse', 'Upload', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', '$q',
|
||||
function HostBrowserServiceFactory(Browse, Upload, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q) {
|
||||
var service = {};
|
||||
|
||||
service.ls = ls;
|
||||
service.get = get;
|
||||
service.delete = deletePath;
|
||||
service.rename = rename;
|
||||
service.upload = upload;
|
||||
|
||||
function ls(path) {
|
||||
return Browse.ls({ path: path }).$promise;
|
||||
}
|
||||
|
||||
function get(path) {
|
||||
return Browse.get({ path: path }).$promise;
|
||||
}
|
||||
|
||||
function deletePath(path) {
|
||||
return Browse.delete({ path: path }).$promise;
|
||||
}
|
||||
|
||||
function rename(path, newPath) {
|
||||
var payload = {
|
||||
CurrentFilePath: path,
|
||||
NewFilePath: newPath
|
||||
};
|
||||
return Browse.rename({}, payload).$promise;
|
||||
}
|
||||
|
||||
function upload(path, file, onProgress) {
|
||||
var deferred = $q.defer();
|
||||
var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/browse/put';
|
||||
Upload.upload({
|
||||
url: url,
|
||||
data: { file: file, Path: path }
|
||||
}).then(deferred.resolve, deferred.reject, onProgress);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
]);
|
||||
@@ -1,27 +1,29 @@
|
||||
angular.module('portainer.agent')
|
||||
.factory('VolumeBrowserService', ['$q', 'Browse', function VolumeBrowserServiceFactory($q, Browse) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
angular.module('portainer.agent').factory('VolumeBrowserService', [
|
||||
'$q', 'Browse',
|
||||
function VolumeBrowserServiceFactory($q, Browse) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.ls = function(volumeId, path) {
|
||||
return Browse.ls({ 'id': volumeId, 'path': path }).$promise;
|
||||
};
|
||||
|
||||
service.get = function(volumeId, path) {
|
||||
return Browse.get({ 'id': volumeId, 'path': path }).$promise;
|
||||
};
|
||||
|
||||
service.delete = function(volumeId, path) {
|
||||
return Browse.delete({ 'id': volumeId, 'path': path }).$promise;
|
||||
};
|
||||
|
||||
service.rename = function(volumeId, path, newPath) {
|
||||
var payload = {
|
||||
CurrentFilePath: path,
|
||||
NewFilePath: newPath
|
||||
service.ls = function(volumeId, path) {
|
||||
return Browse.ls({ volumeID: volumeId, path: path }).$promise;
|
||||
};
|
||||
return Browse.rename({ 'id': volumeId }, payload).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
service.get = function(volumeId, path) {
|
||||
return Browse.get({ volumeID: volumeId, path: path }).$promise;
|
||||
};
|
||||
|
||||
service.delete = function(volumeId, path) {
|
||||
return Browse.delete({ volumeID: volumeId, path: path }).$promise;
|
||||
};
|
||||
|
||||
service.rename = function(volumeId, path, newPath) {
|
||||
var payload = {
|
||||
CurrentFilePath: path,
|
||||
NewFilePath: newPath
|
||||
};
|
||||
return Browse.rename({ volumeID: volumeId }, payload).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -10,6 +10,7 @@ angular.module('portainer')
|
||||
.constant('API_ENDPOINT_STACKS', 'api/stacks')
|
||||
.constant('API_ENDPOINT_STATUS', 'api/status')
|
||||
.constant('API_ENDPOINT_USERS', 'api/users')
|
||||
.constant('API_ENDPOINT_DEPLOYKEYS', 'api/deploykeys')
|
||||
.constant('API_ENDPOINT_TAGS', 'api/tags')
|
||||
.constant('API_ENDPOINT_TEAMS', 'api/teams')
|
||||
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
|
||||
|
||||
@@ -129,13 +129,22 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||
}
|
||||
};
|
||||
|
||||
var engine = {
|
||||
name: 'docker.engine',
|
||||
url: '/engine',
|
||||
var host = {
|
||||
name: 'docker.host',
|
||||
url: '/host',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/docker/views/engine/engine.html',
|
||||
controller: 'EngineController'
|
||||
component: 'hostView'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var hostBrowser = {
|
||||
name: 'docker.host.browser',
|
||||
url: '/browser',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'hostBrowserView'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -239,8 +248,17 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/docker/views/nodes/edit/node.html',
|
||||
controller: 'NodeController'
|
||||
component: 'nodeDetailsView'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var nodeBrowser = {
|
||||
name: 'docker.nodes.node.browse',
|
||||
url: '/browse',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'nodeBrowserView'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -416,6 +434,8 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
$stateRegistryProvider.register(configs);
|
||||
$stateRegistryProvider.register(config);
|
||||
$stateRegistryProvider.register(configCreation);
|
||||
@@ -428,7 +448,8 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||
$stateRegistryProvider.register(containerStats);
|
||||
$stateRegistryProvider.register(docker);
|
||||
$stateRegistryProvider.register(dashboard);
|
||||
$stateRegistryProvider.register(engine);
|
||||
$stateRegistryProvider.register(host);
|
||||
$stateRegistryProvider.register(hostBrowser);
|
||||
$stateRegistryProvider.register(events);
|
||||
$stateRegistryProvider.register(images);
|
||||
$stateRegistryProvider.register(image);
|
||||
@@ -439,6 +460,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||
$stateRegistryProvider.register(networkCreation);
|
||||
$stateRegistryProvider.register(nodes);
|
||||
$stateRegistryProvider.register(node);
|
||||
$stateRegistryProvider.register(nodeBrowser);
|
||||
$stateRegistryProvider.register(secrets);
|
||||
$stateRegistryProvider.register(secret);
|
||||
$stateRegistryProvider.register(secretCreation);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="col-sm-5 col-lg-4">
|
||||
<select class="form-control" ng-model="$ctrl.selectedNetwork" id="container_network">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in $ctrl.availableNetworks" ng-value="net.Id">{{ net.Name }}</option>
|
||||
<option ng-repeat="net in $ctrl.availableNetworks | orderBy: 'Name'" ng-value="net.Id">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
|
||||
@@ -72,6 +72,7 @@ function ($state, ContainerService, ModalService, Notifications, HttpRequestHelp
|
||||
Notifications.success(successMessage, container.Names[0]);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
errorMessage = errorMessage + ':' + container.Names[0];
|
||||
Notifications.error('Failure', err, errorMessage);
|
||||
})
|
||||
.finally(function final() {
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
<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.nodes.node({id: item.Id})" ng-if="$ctrl.accessToNodeDetails">{{ item.Hostname }}</a>
|
||||
<span ng-if="!$ctrl.accessToNodeDetails">{{ item.Hostname }}</span>
|
||||
<a ui-sref="docker.nodes.node({id: item.Id})" ng-if="$ctrl.accessToNodeDetails">{{ item.Name || item.Hostname }}</a>
|
||||
<span ng-if="!$ctrl.accessToNodeDetails">{{ item.Name || item.Hostname }}</span>
|
||||
</td>
|
||||
<td>{{ item.Role }}</td>
|
||||
<td>{{ item.CPUs / 1000000000 }}</td>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
angular.module('portainer.docker').component('dockerSidebarContent', {
|
||||
templateUrl: 'app/docker/components/dockerSidebarContent/dockerSidebarContent.html',
|
||||
bindings: {
|
||||
'endpointApiVersion': '<',
|
||||
'swarmManagement': '<',
|
||||
'standaloneManagement': '<',
|
||||
'adminAccess': '<'
|
||||
endpointApiVersion: '<',
|
||||
swarmManagement: '<',
|
||||
standaloneManagement: '<',
|
||||
adminAccess: '<'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
<a ui-sref="docker.swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement">
|
||||
<a ui-sref="docker.engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th fa-fw"></span></a>
|
||||
<a ui-sref="docker.host" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
|
||||
</li>
|
||||
|
||||
21
app/docker/components/host-overview/host-overview.html
Normal file
21
app/docker/components/host-overview/host-overview.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Host overview">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="{{$ctrl.refreshUrl}}"
|
||||
ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Docker</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<host-details-panel
|
||||
host="$ctrl.hostDetails"
|
||||
is-agent="$ctrl.isAgent"
|
||||
browse-url="{{$ctrl.browseUrl}}"></host-details-panel>
|
||||
|
||||
<engine-details-panel engine="$ctrl.engineDetails"></engine-details-panel>
|
||||
|
||||
<devices-panel ng-if="$ctrl.isAgent" devices="$ctrl.devices"></devices-panel>
|
||||
<disks-panel ng-if="$ctrl.isAgent" disks="$ctrl.disks"></disks-panel>
|
||||
|
||||
<ng-transclude></ng-transclude>
|
||||
13
app/docker/components/host-overview/host-overview.js
Normal file
13
app/docker/components/host-overview/host-overview.js
Normal file
@@ -0,0 +1,13 @@
|
||||
angular.module('portainer.docker').component('hostOverview', {
|
||||
templateUrl: 'app/docker/components/host-overview/host-overview.html',
|
||||
bindings: {
|
||||
hostDetails: '<',
|
||||
engineDetails: '<',
|
||||
devices: '<',
|
||||
disks: '<',
|
||||
isAgent: '<',
|
||||
refreshUrl: '@',
|
||||
browseUrl: '@'
|
||||
},
|
||||
transclude: true
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-code" title-text="PCI Devices"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Vendor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-if="$ctrl.devices && $ctrl.devices.length" ng-repeat="device in $ctrl.devices">
|
||||
<td>{{device.Name}}</td>
|
||||
<td>{{device.Vendor}}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.devices">
|
||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.devices.length === 0">
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
No device available.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
angular.module('portainer.docker').component('devicesPanel', {
|
||||
templateUrl:
|
||||
'app/docker/components/host-view-panels/devices-panel/devices-panel.html',
|
||||
bindings: {
|
||||
devices: '<'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-code" title-text="Physical Disks"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vendor</th>
|
||||
<th>Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-if="$ctrl.disks" ng-repeat="disk in $ctrl.disks">
|
||||
<td>{{disk.Vendor}}</td>
|
||||
<td>{{disk.Size | humansize}}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.disks">
|
||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.disks.length === 0">
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
No disks available.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
angular.module('portainer.docker').component('disksPanel', {
|
||||
templateUrl:
|
||||
'app/docker/components/host-view-panels/disks-panel/disks-panel.html',
|
||||
bindings: {
|
||||
disks: '<'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-code" title-text="Engine Details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>{{ $ctrl.engine.releaseVersion }} <span ng-if="$ctrl.engine.apiVersion">(API: {{ $ctrl.engine.apiVersion }})</span></td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.engine.rootDirectory">
|
||||
<td>Root directory</td>
|
||||
<td>{{ $ctrl.engine.rootDirectory }}</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.engine.storageDriver">
|
||||
<td>Storage Driver</td>
|
||||
<td>{{ $ctrl.engine.storageDriver }}</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.engine.loggingDriver">
|
||||
<td>Logging Driver</td>
|
||||
<td>{{ $ctrl.engine.loggingDriver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Volume Plugins</td>
|
||||
<td>{{ $ctrl.engine.volumePlugins | arraytostr: ', ' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Network Plugins</td>
|
||||
<td>{{ $ctrl.engine.networkPlugins | arraytostr: ', ' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
angular.module('portainer.docker').component('engineDetailsPanel', {
|
||||
templateUrl:
|
||||
'app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.html',
|
||||
bindings: {
|
||||
engine: '<'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-code" title-text="Host Details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Hostname</td>
|
||||
<td>{{ $ctrl.host.name }}</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.host.os">
|
||||
<td>OS Information</td>
|
||||
<td>{{ $ctrl.host.os.type }} {{$ctrl.host.os.arch}}
|
||||
{{$ctrl.host.os.name}}</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.host.kernelVersion">
|
||||
<td>Kernel Version</td>
|
||||
<td>{{ $ctrl.host.kernelVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total CPU</td>
|
||||
<td>{{ $ctrl.host.totalCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total memory</td>
|
||||
<td>{{ $ctrl.host.totalMemory | humansize }}</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.isAgent">
|
||||
<td colspan="2">
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
title="Browse"
|
||||
ui-sref="{{$ctrl.browseUrl}}">
|
||||
Browse
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
angular.module('portainer.docker').component('hostDetailsPanel', {
|
||||
templateUrl:
|
||||
'app/docker/components/host-view-panels/host-details-panel/host-details-panel.html',
|
||||
bindings: {
|
||||
host: '<',
|
||||
isAgent: '<',
|
||||
browseUrl: '@'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
angular
|
||||
.module('portainer.docker')
|
||||
.controller('NodeAvailabilitySelectController', [
|
||||
function NodeAvailabilitySelectController() {
|
||||
this.onChange = onChange;
|
||||
|
||||
function onChange() {
|
||||
this.onSave({ availability: this.availability });
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="input-group input-group-sm">
|
||||
<select name="nodeAvailability" class="selectpicker form-control" ng-model="$ctrl.availability"
|
||||
ng-change="$ctrl.onChange()">
|
||||
<option value="active">Active</option>
|
||||
<option value="pause">Pause</option>
|
||||
<option value="drain">Drain</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
angular.module('portainer.docker').component('nodeAvailabilitySelect', {
|
||||
templateUrl:
|
||||
'app/docker/components/host-view-panels/node-availability-select/node-availability-select.html',
|
||||
controller: 'NodeAvailabilitySelectController',
|
||||
bindings: {
|
||||
availability: '<',
|
||||
originalValue: '<',
|
||||
onSave: '&'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
angular.module('portainer.docker').controller('NodeLabelsTableController', [
|
||||
function NodeLabelsTableController() {
|
||||
var ctrl = this;
|
||||
ctrl.removeLabel = removeLabel;
|
||||
ctrl.updateLabel = updateLabel;
|
||||
|
||||
function removeLabel(index) {
|
||||
var label = ctrl.labels.splice(index, 1);
|
||||
if (label !== null) {
|
||||
ctrl.onChangedLabels({ labels: ctrl.labels });
|
||||
}
|
||||
}
|
||||
|
||||
function updateLabel(label) {
|
||||
if (
|
||||
label.value !== label.originalValue ||
|
||||
label.key !== label.originalKey
|
||||
) {
|
||||
ctrl.onChangedLabels({ labels: ctrl.labels });
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -0,0 +1,35 @@
|
||||
<div ng-if="!$ctrl.labels.length">
|
||||
There are no labels for this node.
|
||||
</div>
|
||||
|
||||
<table class="table" ng-if="$ctrl.labels.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="label in $ctrl.labels">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">Name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo"
|
||||
ng-change="$ctrl.updateLabel(label)">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">Value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar"
|
||||
ng-change="$ctrl.updateLabel(label)">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeLabel($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,9 @@
|
||||
angular.module('portainer.docker').component('nodeLabelsTable', {
|
||||
templateUrl:
|
||||
'app/docker/components/host-view-panels/node-labels-table/node-labels-table.html',
|
||||
controller: 'NodeLabelsTableController',
|
||||
bindings: {
|
||||
labels: '<',
|
||||
onChangedLabels: '&'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
angular
|
||||
.module('portainer.docker')
|
||||
.controller('SwarmNodeDetailsPanelController', [
|
||||
'NodeService', 'LabelHelper', 'Notifications', '$state',
|
||||
function SwarmNodeDetailsPanelController(NodeService, LabelHelper, Notifications, $state) {
|
||||
var ctrl = this;
|
||||
ctrl.state = {
|
||||
managerAddress: '',
|
||||
hasChanges: false
|
||||
};
|
||||
ctrl.$onChanges = $onChanges;
|
||||
ctrl.addLabel = addLabel;
|
||||
ctrl.updateNodeLabels = updateNodeLabels;
|
||||
ctrl.updateNodeAvailability = updateNodeAvailability;
|
||||
ctrl.saveChanges = saveChanges;
|
||||
ctrl.cancelChanges = cancelChanges;
|
||||
|
||||
var managerRole = 'manager';
|
||||
|
||||
function $onChanges() {
|
||||
if (!ctrl.details) {
|
||||
return;
|
||||
}
|
||||
if (ctrl.details.role === managerRole) {
|
||||
ctrl.state.managerAddress = '(' + ctrl.details.managerAddress + ')';
|
||||
}
|
||||
}
|
||||
|
||||
function addLabel() {
|
||||
ctrl.details.nodeLabels.push({
|
||||
key: '',
|
||||
value: '',
|
||||
originalValue: '',
|
||||
originalKey: ''
|
||||
});
|
||||
}
|
||||
|
||||
function updateNodeLabels(labels) {
|
||||
ctrl.details.nodeLabels = labels;
|
||||
ctrl.state.hasChanges = true;
|
||||
}
|
||||
|
||||
function updateNodeAvailability(availability) {
|
||||
ctrl.details.availability = availability;
|
||||
ctrl.state.hasChanges = true;
|
||||
}
|
||||
|
||||
function saveChanges() {
|
||||
var originalNode = ctrl.originalNode;
|
||||
var config = {
|
||||
Name: originalNode.Name,
|
||||
Availability: ctrl.details.availability,
|
||||
Role: originalNode.Role,
|
||||
Labels: LabelHelper.fromKeyValueToLabelHash(ctrl.details.nodeLabels),
|
||||
Id: originalNode.Id,
|
||||
Version: originalNode.Version
|
||||
};
|
||||
|
||||
NodeService.updateNode(config)
|
||||
.then(onUpdateSuccess)
|
||||
.catch(notifyOnError);
|
||||
|
||||
function onUpdateSuccess() {
|
||||
Notifications.success('Node successfully updated', 'Node updated');
|
||||
$state.go(
|
||||
'docker.nodes.node',
|
||||
{ id: originalNode.Id },
|
||||
{ reload: true }
|
||||
);
|
||||
}
|
||||
|
||||
function notifyOnError(error) {
|
||||
Notifications.error('Failure', error, 'Failed to update node');
|
||||
}
|
||||
}
|
||||
|
||||
function cancelChanges() {
|
||||
cancelLabelChanges();
|
||||
ctrl.details.availability = ctrl.originalNode.Availability;
|
||||
ctrl.state.hasChanges = false;
|
||||
}
|
||||
|
||||
function cancelLabelChanges() {
|
||||
ctrl.details.nodeLabels = ctrl.details.nodeLabels
|
||||
.filter(function(label) {
|
||||
return label.originalValue || label.originalKey;
|
||||
})
|
||||
.map(function(label) {
|
||||
return Object.assign(label, {
|
||||
value: label.originalValue,
|
||||
key: label.originalKey
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -0,0 +1,75 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-code" title-text="Node Details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr ng-if="$ctrl.details.name">
|
||||
<td>Node name</td>
|
||||
<td>{{ $ctrl.details.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>{{ $ctrl.details.role }} {{ $ctrl.state.managerAddress }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Availability</td>
|
||||
<td>
|
||||
<node-availability-select on-save="$ctrl.updateNodeAvailability(availability)"
|
||||
availability="$ctrl.details.availability" original-value="$ctrl.details.availability">
|
||||
</node-availability-select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><span class="label label-{{ $ctrl.details.status | nodestatusbadge }}">{{
|
||||
$ctrl.details.status }}</span></td>
|
||||
</tr>
|
||||
<tr ng-if=" $ctrl.details.engineLabels.length">
|
||||
<td>Engine Labels</td>
|
||||
<td>{{ $ctrl.details.engineLabels | arraytostr:', ' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="$ctrl.addLabel(node)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
|
||||
</a>
|
||||
</div>
|
||||
Node Labels
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<node-labels-table labels="$ctrl.details.nodeLabels"
|
||||
on-changed-labels="$ctrl.updateNodeLabels(labels)"></node-labels-table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
ng-disabled="!$ctrl.state.hasChanges" ng-click="$ctrl.saveChanges()">
|
||||
Apply changes
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="$ctrl.cancelChanges()">Reset changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
angular.module('portainer.docker').component('swarmNodeDetailsPanel', {
|
||||
templateUrl:
|
||||
'app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.html',
|
||||
controller: 'SwarmNodeDetailsPanelController',
|
||||
bindings: {
|
||||
details: '<',
|
||||
originalNode: '<'
|
||||
}
|
||||
});
|
||||
@@ -1,24 +1,48 @@
|
||||
angular.module('portainer.docker')
|
||||
.factory('NodeService', ['$q', 'Node', function NodeServiceFactory($q, Node) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
angular.module('portainer.docker').factory('NodeService', [
|
||||
'$q', 'Node',
|
||||
function NodeServiceFactory($q, Node) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.nodes = function() {
|
||||
var deferred = $q.defer();
|
||||
service.nodes = nodes;
|
||||
service.node = node;
|
||||
service.updateNode = updateNode;
|
||||
|
||||
Node.query({}).$promise
|
||||
.then(function success(data) {
|
||||
var nodes = data.map(function (item) {
|
||||
return new NodeViewModel(item);
|
||||
});
|
||||
deferred.resolve(nodes);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve nodes', err: err });
|
||||
});
|
||||
function node(id) {
|
||||
var deferred = $q.defer();
|
||||
Node.get({ id: id })
|
||||
.$promise.then(function onNodeLoaded(rawNode) {
|
||||
var node = new NodeViewModel(rawNode);
|
||||
return deferred.resolve(node);
|
||||
})
|
||||
.catch(function onFailed(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve node', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
return service;
|
||||
}]);
|
||||
function nodes() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Node.query({})
|
||||
.$promise.then(function success(data) {
|
||||
var nodes = data.map(function(item) {
|
||||
return new NodeViewModel(item);
|
||||
});
|
||||
deferred.resolve(nodes);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve nodes', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function updateNode(node) {
|
||||
return Node.update({ id: node.Id, version: node.Version }, node).$promise;
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -544,14 +544,8 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
||||
|
||||
SystemService.info()
|
||||
.then(function success(data) {
|
||||
var runtimes = data.Runtimes;
|
||||
$scope.availableRuntimes = runtimes;
|
||||
if ('runc' in runtimes) {
|
||||
$scope.config.HostConfig.Runtime = 'runc';
|
||||
}
|
||||
else if (Object.keys(runtimes).length !== 0) {
|
||||
$scope.config.HostConfig.Runtime = Object.keys(runtimes)[0];
|
||||
}
|
||||
$scope.availableRuntimes = Object.keys(data.Runtimes);
|
||||
$scope.config.HostConfig.Runtime = '';
|
||||
$scope.state.sliderMaxCpu = 32;
|
||||
if (data.NCPU) {
|
||||
$scope.state.sliderMaxCpu = data.NCPU;
|
||||
|
||||
@@ -309,7 +309,7 @@
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" ng-model="config.HostConfig.NetworkMode" id="container_network" ng-change="resetNetworkConfig()">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
<option ng-repeat="net in availableNetworks | orderBy: 'Name'" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -503,11 +503,11 @@
|
||||
<!-- !privileged-mode -->
|
||||
<!-- runtimes -->
|
||||
<div class="form-group">
|
||||
<label for="container_runtime" class="col-sm-2 col-lg-1 control-label text-left">Runtime</label>
|
||||
<div class="col-sm-1">
|
||||
<select class="form-control" ng-model="config.HostConfig.Runtime" id="container_runtime">
|
||||
<option selected disabled hidden value="">Select a runtime</option>
|
||||
<option ng-repeat="(runtime, _) in availableRuntimes" ng-value="runtime">{{ runtime }}</option>
|
||||
<label for="container_runtime" class="col-sm-1 control-label text-left">Runtime</label>
|
||||
<div class="col-sm-11">
|
||||
<select class="form-control" ng-model="config.HostConfig.Runtime"
|
||||
id="container_runtime" ng-options="runtime for runtime in availableRuntimes">
|
||||
<option selected value="">Default</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Engine overview">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="docker.engine" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Docker</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="info && version">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-code" title-text="Engine version"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>{{ version.Version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>API version</td>
|
||||
<td>{{ version.ApiVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Go version</td>
|
||||
<td>{{ version.GoVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OS type</td>
|
||||
<td>{{ version.Os }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OS</td>
|
||||
<td>{{ info.OperatingSystem }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Architecture</td>
|
||||
<td>{{ version.Arch }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kernel version</td>
|
||||
<td>{{ version.KernelVersion }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="info && version">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-th" title-text="Engine status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Total CPU</td>
|
||||
<td>{{ info.NCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total memory</td>
|
||||
<td>{{ info.MemTotal|humansize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker root directory</td>
|
||||
<td>{{ info.DockerRootDir }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Storage driver</td>
|
||||
<td>{{ info.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Logging driver</td>
|
||||
<td>{{ info.LoggingDriver }}</td>
|
||||
</tr>
|
||||
<tr ng-if="info.CgroupDriver">
|
||||
<td>Cgroup driver</td>
|
||||
<td>{{ info.CgroupDriver }}</td>
|
||||
</tr>
|
||||
<tr ng-if="info.ExecutionDriver">
|
||||
<td>Execution driver</td>
|
||||
<td>{{ info.ExecutionDriver }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="info && info.Plugins">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plug" title-text="Engine plugins"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr ng-if="info.Plugins.Volume">
|
||||
<td>Volume</td>
|
||||
<td>{{ info.Plugins.Volume|arraytostr: ', '}}</td>
|
||||
</tr>
|
||||
<tr ng-if="info.Plugins.Network">
|
||||
<td>Network</td>
|
||||
<td>{{ info.Plugins.Network|arraytostr: ', '}}</td>
|
||||
</tr>
|
||||
<tr ng-if="info.Plugins.Authorization">
|
||||
<td>Authorization</td>
|
||||
<td>{{ info.Plugins.Authorization|arraytostr: ', '}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,22 +0,0 @@
|
||||
angular.module('portainer.docker')
|
||||
.controller('EngineController', ['$q', '$scope', 'SystemService', 'Notifications',
|
||||
function ($q, $scope, SystemService, Notifications) {
|
||||
|
||||
function initView() {
|
||||
$q.all({
|
||||
version: SystemService.version(),
|
||||
info: SystemService.info()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.version = data.version;
|
||||
$scope.info = data.info;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.info = {};
|
||||
$scope.version = {};
|
||||
Notifications.error('Failure', err, 'Unable to retrieve engine details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
@@ -0,0 +1,21 @@
|
||||
angular
|
||||
.module('portainer.docker')
|
||||
.controller('HostBrowserViewController', [
|
||||
'SystemService', 'HttpRequestHelper',
|
||||
function HostBrowserViewController(SystemService, HttpRequestHelper) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.$onInit = $onInit;
|
||||
|
||||
function $onInit() {
|
||||
loadInfo();
|
||||
}
|
||||
|
||||
function loadInfo() {
|
||||
SystemService.info().then(function onInfoLoaded(host) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(host.Name);
|
||||
ctrl.host = host;
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -0,0 +1,14 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Host Browser"></rd-header-title>
|
||||
<rd-header-content>
|
||||
Host > <a ui-sref="docker.host">{{ $ctrl.host.Name }}</a> > browse
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<host-browser
|
||||
ng-if="$ctrl.host"
|
||||
></host-browser>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
angular.module('portainer.docker').component('hostBrowserView', {
|
||||
templateUrl: 'app/docker/views/host/host-browser-view/host-browser-view.html',
|
||||
controller: 'HostBrowserViewController'
|
||||
});
|
||||
70
app/docker/views/host/host-view-controller.js
Normal file
70
app/docker/views/host/host-view-controller.js
Normal file
@@ -0,0 +1,70 @@
|
||||
angular.module('portainer.docker').controller('HostViewController', [
|
||||
'$q', 'SystemService', 'Notifications', 'StateManager', 'AgentService',
|
||||
function HostViewController($q, SystemService, Notifications, StateManager, AgentService) {
|
||||
var ctrl = this;
|
||||
this.$onInit = initView;
|
||||
|
||||
ctrl.state = {
|
||||
isAgent: false
|
||||
};
|
||||
|
||||
this.engineDetails = {};
|
||||
this.hostDetails = {};
|
||||
|
||||
function initView() {
|
||||
var applicationState = StateManager.getState();
|
||||
ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy;
|
||||
|
||||
$q.all({
|
||||
version: SystemService.version(),
|
||||
info: SystemService.info()
|
||||
})
|
||||
.then(function success(data) {
|
||||
ctrl.engineDetails = buildEngineDetails(data);
|
||||
ctrl.hostDetails = buildHostDetails(data.info);
|
||||
|
||||
if (ctrl.state.isAgent) {
|
||||
return AgentService.hostInfo(data.info.Hostname).then(function onHostInfoLoad(agentHostInfo) {
|
||||
ctrl.devices = agentHostInfo.PCIDevices;
|
||||
ctrl.disks = agentHostInfo.PhysicalDisks;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error(
|
||||
'Failure',
|
||||
err,
|
||||
'Unable to retrieve engine details'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function buildEngineDetails(data) {
|
||||
var versionDetails = data.version;
|
||||
var info = data.info;
|
||||
return {
|
||||
releaseVersion: versionDetails.Version,
|
||||
apiVersion: versionDetails.ApiVersion,
|
||||
rootDirectory: info.DockerRootDir,
|
||||
storageDriver: info.Driver,
|
||||
loggingDriver: info.LoggingDriver,
|
||||
volumePlugins: info.Plugins.Volume,
|
||||
networkPlugins: info.Plugins.Network
|
||||
};
|
||||
}
|
||||
|
||||
function buildHostDetails(info) {
|
||||
return {
|
||||
os: {
|
||||
arch: info.Architecture,
|
||||
type: info.OSType,
|
||||
name: info.OperatingSystem
|
||||
},
|
||||
name: info.Name,
|
||||
kernelVersion: info.KernelVersion,
|
||||
totalCPU: info.NCPU,
|
||||
totalMemory: info.MemTotal
|
||||
};
|
||||
}
|
||||
}
|
||||
]);
|
||||
10
app/docker/views/host/host-view.html
Normal file
10
app/docker/views/host/host-view.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<host-overview
|
||||
engine-details="$ctrl.engineDetails"
|
||||
host-details="$ctrl.hostDetails"
|
||||
is-agent="$ctrl.state.isAgent"
|
||||
disks="$ctrl.disks"
|
||||
devices="$ctrl.devices"
|
||||
|
||||
refresh-url="docker.host"
|
||||
browse-url="docker.host.browser"
|
||||
></host-overview>
|
||||
4
app/docker/views/host/host-view.js
Normal file
4
app/docker/views/host/host-view.js
Normal file
@@ -0,0 +1,4 @@
|
||||
angular.module('portainer.docker').component('hostView', {
|
||||
templateUrl: 'app/docker/views/host/host-view.html',
|
||||
controller: 'HostViewController'
|
||||
});
|
||||
@@ -1,243 +0,0 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Node details">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="docker.nodes.node({id: node.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="docker.swarm">Swarm nodes</a> > <a ui-sref="docker.nodes.node({id: node.Id})">{{ node.Hostname }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="!node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div ng-if="loading">
|
||||
<i class="fa fa-cog fa-spin"></i> Loading...
|
||||
</div>
|
||||
|
||||
<rd-widget ng-if="!loading">
|
||||
<rd-widget-header icon="fa-object-group" title-text="Node does not exist"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<p>It looks like the node you wish to inspect does not exist.</p>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title-text="Node specification"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<input type="text" class="input-sm" ng-model="node.Name" placeholder="e.g. my-manager" ng-change="updateNodeAttribute(node, 'Name')">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Host name</td>
|
||||
<td>{{ node.Hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>{{ node.Role }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Availability</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<select name="nodeAvailability" class="selectpicker form-control" ng-model="node.Availability" ng-change="updateNodeAttribute(node, 'Availability')">
|
||||
<option value="active">Active</option>
|
||||
<option value="pause">Pause</option>
|
||||
<option value="drain">Drain</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><span class="label label-{{ node.Status|nodestatusbadge }}">{{ node.Status }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<p class="small text-muted">
|
||||
View the Docker Swarm mode Node documentation <a href="https://docs.docker.com/engine/swarm/manage-nodes/" target="self">here</a>.
|
||||
</p>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary" ng-disabled="!hasChanges(node, ['Name', 'Availability'])" ng-click="updateNode(node)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(node)">Reset changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node && node.Role === 'manager'">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title-text="Manager status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Leader</td>
|
||||
<td>
|
||||
<span ng-if="node.Leader"><i class="fa fa-check green-icon" aria-hidden="true"></i> Yes</span>
|
||||
<span ng-if="!node.Leader"><i class="fa fa-times red-icon" aria-hidden="true"></i> No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Reachability</td>
|
||||
<td>{{ node.Reachability }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manager address</td>
|
||||
<td>{{ node.ManagerAddr }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title-text="Node description"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>CPU</td>
|
||||
<td>{{ node.CPUs / 1000000000 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory</td>
|
||||
<td>{{ node.Memory|humansize: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Platform</td>
|
||||
<td>{{ node.PlatformOS }} {{ node.PlatformArchitecture }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker Engine version</td>
|
||||
<td>{{ node.EngineVersion }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title-text="Engine labels"></rd-widget-header>
|
||||
<rd-widget-body ng-if="!node.EngineLabels || node.EngineLabels.length === 0">
|
||||
<p>There are no engine labels for this node.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body classes="no-padding" ng-if="node.EngineLabels && node.EngineLabels.length > 0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="engineLabel in node.EngineLabels">
|
||||
<td>{{ engineLabel.key }}</td>
|
||||
<td>{{ engineLabel.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title-text="Node labels">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="addLabel(node)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="!node.Labels || node.Labels.length === 0">
|
||||
<p>There are no labels for this node.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body classes="no-padding" ng-if="node.Labels && node.Labels.length > 0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="label in node.Labels">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(node, label)">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(node, label)">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel(node, $index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(node, ['Labels'])" ng-click="updateNode(node)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(node)">Reset changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node && tasks.length > 0">
|
||||
<div class="col-sm-12">
|
||||
<node-tasks-datatable
|
||||
title-text="Tasks" title-icon="fa-tasks"
|
||||
dataset="tasks" table-key="node-tasks"
|
||||
order-by="Updated" reverse-order="true"
|
||||
|
||||
></node-tasks-datatable>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,96 +0,0 @@
|
||||
angular.module('portainer.docker')
|
||||
.controller('NodeController', ['$scope', '$state', '$transition$', 'LabelHelper', 'Node', 'NodeHelper', 'Task', 'Notifications',
|
||||
function ($scope, $state, $transition$, LabelHelper, Node, NodeHelper, Task, Notifications) {
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.tasks = [];
|
||||
|
||||
var originalNode = {};
|
||||
var editedKeys = [];
|
||||
|
||||
$scope.updateNodeAttribute = function updateNodeAttribute(node, key) {
|
||||
editedKeys.push(key);
|
||||
};
|
||||
$scope.addLabel = function addLabel(node) {
|
||||
node.Labels.push({ key: '', value: '', originalValue: '', originalKey: '' });
|
||||
$scope.updateNodeAttribute(node, 'Labels');
|
||||
};
|
||||
$scope.removeLabel = function removeLabel(node, index) {
|
||||
var removedElement = node.Labels.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
$scope.updateNodeAttribute(node, 'Labels');
|
||||
}
|
||||
};
|
||||
$scope.updateLabel = function updateLabel(node, label) {
|
||||
if (label.value !== label.originalValue || label.key !== label.originalKey) {
|
||||
$scope.updateNodeAttribute(node, 'Labels');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.hasChanges = function(node, elements) {
|
||||
if (!elements) {
|
||||
elements = Object.keys(originalNode);
|
||||
}
|
||||
var hasChanges = false;
|
||||
elements.forEach(function(key) {
|
||||
hasChanges = hasChanges || ((editedKeys.indexOf(key) >= 0) && node[key] !== originalNode[key]);
|
||||
});
|
||||
return hasChanges;
|
||||
};
|
||||
|
||||
$scope.cancelChanges = function(node) {
|
||||
editedKeys.forEach(function(key) {
|
||||
node[key] = originalNode[key];
|
||||
});
|
||||
editedKeys = [];
|
||||
};
|
||||
|
||||
$scope.updateNode = function updateNode(node) {
|
||||
var config = NodeHelper.nodeToConfig(node.Model);
|
||||
config.Name = node.Name;
|
||||
config.Availability = node.Availability;
|
||||
config.Role = node.Role;
|
||||
config.Labels = LabelHelper.fromKeyValueToLabelHash(node.Labels);
|
||||
|
||||
Node.update({ id: node.Id, version: node.Version }, config, function () {
|
||||
Notifications.success('Node successfully updated', 'Node updated');
|
||||
$state.go('docker.nodes.node', {id: node.Id}, {reload: true});
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Failed to update node');
|
||||
});
|
||||
};
|
||||
|
||||
function loadNodeAndTasks() {
|
||||
$scope.loading = true;
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
Node.get({ id: $transition$.params().id}, function(d) {
|
||||
if (d.message) {
|
||||
Notifications.error('Failure', e, 'Unable to inspect the node');
|
||||
} else {
|
||||
var node = new NodeViewModel(d);
|
||||
originalNode = angular.copy(node);
|
||||
$scope.node = node;
|
||||
getTasks(d);
|
||||
}
|
||||
$scope.loading = false;
|
||||
});
|
||||
} else {
|
||||
$scope.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getTasks(node) {
|
||||
if (node) {
|
||||
Task.query({filters: {node: [node.ID]}}, function (tasks) {
|
||||
$scope.tasks = tasks.map(function (task) {
|
||||
return new TaskViewModel(task);
|
||||
});
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve tasks associated to the node');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadNodeAndTasks();
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,20 @@
|
||||
angular.module('portainer.docker').controller('NodeBrowserController', [
|
||||
'NodeService', 'HttpRequestHelper', '$stateParams',
|
||||
function NodeBrowserController(NodeService, HttpRequestHelper, $stateParams) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.$onInit = $onInit;
|
||||
|
||||
function $onInit() {
|
||||
ctrl.nodeId = $stateParams.id;
|
||||
loadNode();
|
||||
}
|
||||
|
||||
function loadNode() {
|
||||
NodeService.node(ctrl.nodeId).then(function onNodeLoaded(node) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(node.Hostname);
|
||||
ctrl.node = node;
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
14
app/docker/views/nodes/node-browser/node-browser.html
Normal file
14
app/docker/views/nodes/node-browser/node-browser.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Node Browser"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="docker.swarm">Swarm</a> > <a ui-sref="docker.nodes.node({ id: $ctrl.nodeId })">{{ $ctrl.node.Hostname }}</a> > browse
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<host-browser
|
||||
ng-if="$ctrl.node"
|
||||
></host-browser>
|
||||
</div>
|
||||
</div>
|
||||
4
app/docker/views/nodes/node-browser/node-browser.js
Normal file
4
app/docker/views/nodes/node-browser/node-browser.js
Normal file
@@ -0,0 +1,4 @@
|
||||
angular.module('portainer.docker').component('nodeBrowserView', {
|
||||
templateUrl: 'app/docker/views/nodes/node-browser/node-browser.html',
|
||||
controller: 'NodeBrowserController'
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
angular.module('portainer.docker').controller('NodeDetailsViewController', [
|
||||
'$stateParams', 'NodeService', 'StateManager', 'AgentService',
|
||||
function NodeDetailsViewController($stateParams, NodeService, StateManager, AgentService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.$onInit = initView;
|
||||
|
||||
ctrl.state = {
|
||||
isAgent: false
|
||||
};
|
||||
|
||||
function initView() {
|
||||
var applicationState = StateManager.getState();
|
||||
ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy;
|
||||
|
||||
var nodeId = $stateParams.id;
|
||||
NodeService.node(nodeId).then(function(node) {
|
||||
ctrl.originalNode = node;
|
||||
ctrl.hostDetails = buildHostDetails(node);
|
||||
ctrl.engineDetails = buildEngineDetails(node);
|
||||
ctrl.nodeDetails = buildNodeDetails(node);
|
||||
if (ctrl.state.isAgent) {
|
||||
AgentService.hostInfo(node.Hostname).then(function onHostInfoLoad(
|
||||
agentHostInfo
|
||||
) {
|
||||
ctrl.devices = agentHostInfo.PCIDevices;
|
||||
ctrl.disks = agentHostInfo.PhysicalDisks;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildHostDetails(node) {
|
||||
return {
|
||||
os: {
|
||||
arch: node.PlatformArchitecture,
|
||||
type: node.PlatformOS
|
||||
},
|
||||
name: node.Hostname,
|
||||
totalCPU: node.CPUs / 1e9,
|
||||
totalMemory: node.Memory
|
||||
};
|
||||
}
|
||||
|
||||
function buildEngineDetails(node) {
|
||||
return {
|
||||
releaseVersion: node.EngineVersion,
|
||||
volumePlugins: transformPlugins(node.Plugins, 'Volume'),
|
||||
networkPlugins: transformPlugins(node.Plugins, 'Network')
|
||||
};
|
||||
}
|
||||
|
||||
function buildNodeDetails(node) {
|
||||
return {
|
||||
name: node.Name,
|
||||
role: node.Role,
|
||||
managerAddress: node.ManagerAddr,
|
||||
availability: node.Availability,
|
||||
status: node.Status,
|
||||
engineLabels: node.EngineLabels,
|
||||
nodeLabels: node.Labels
|
||||
};
|
||||
}
|
||||
|
||||
function transformPlugins(pluginsList, type) {
|
||||
return pluginsList
|
||||
.filter(function(plugin) {
|
||||
return plugin.Type === type;
|
||||
})
|
||||
.map(function(plugin) {
|
||||
return plugin.Name;
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
15
app/docker/views/nodes/node-details/node-details-view.html
Normal file
15
app/docker/views/nodes/node-details/node-details-view.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<host-overview
|
||||
is-agent="$ctrl.state.isAgent"
|
||||
host-details="$ctrl.hostDetails"
|
||||
engine-details="$ctrl.engineDetails"
|
||||
disks="$ctrl.disks"
|
||||
devices="$ctrl.devices"
|
||||
|
||||
refresh-url="docker.nodes.node"
|
||||
browse-url="docker.nodes.node.browse"
|
||||
>
|
||||
<swarm-node-details-panel
|
||||
details="$ctrl.nodeDetails"
|
||||
original-node="$ctrl.originalNode"
|
||||
></swarm-node-details-panel>
|
||||
</host-overview>
|
||||
4
app/docker/views/nodes/node-details/node-details-view.js
Normal file
4
app/docker/views/nodes/node-details/node-details-view.js
Normal file
@@ -0,0 +1,4 @@
|
||||
angular.module('portainer.docker').component('nodeDetailsView', {
|
||||
templateUrl: 'app/docker/views/nodes/node-details/node-details-view.html',
|
||||
controller: 'NodeDetailsViewController'
|
||||
});
|
||||
@@ -354,7 +354,7 @@
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" ng-model="formValues.Network">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
<option ng-repeat="net in availableNetworks | orderBy: 'Name'" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<div class="node_info">
|
||||
<div>
|
||||
<div>
|
||||
<b>{{ node.Hostname }}</b>
|
||||
<b>{{ node.Name || node.Hostname }}</b>
|
||||
<span class="node_platform">
|
||||
<i class="fab fa-linux" aria-hidden="true" ng-if="node.PlatformOS === 'linux'"></i>
|
||||
<i class="fab fa-windows" aria-hidden="true" ng-if="node.PlatformOS === 'windows'"></i>
|
||||
@@ -97,7 +97,7 @@
|
||||
<div><span class="label label-{{ node.Status | nodestatusbadge }}">{{ node.Status }}</span></div>
|
||||
</div>
|
||||
<div class="tasks">
|
||||
<div class="task task_{{ task.Status.State | visualizerTask }}" style="border: 2px solid {{ task.ServiceId | visualizerTaskBorderColor }}" ng-repeat="task in node.Tasks | filter: (state.DisplayOnlyRunningTasks || '') && { Status: { State: 'running' } }">
|
||||
<div class="task task_{{ task.Status.State | visualizerTask }}" style="border: 2px solid {{ task.ServiceId | visualizerTaskBorderColor }}" ng-repeat="task in node.Tasks | orderBy: 'ServiceName' | filter: (state.DisplayOnlyRunningTasks || '') && { Status: { State: 'running' } }">
|
||||
<div class="service_name">{{ task.ServiceName }}</div>
|
||||
<div>Image: {{ task.Spec.ContainerSpec.Image | hideshasum }}</div>
|
||||
<div>Status: {{ task.Status.State }}</div>
|
||||
|
||||
@@ -287,8 +287,8 @@ angular.module('portainer.app', [])
|
||||
};
|
||||
|
||||
var stackCreation = {
|
||||
name: 'portainer.stacks.new',
|
||||
url: '/new',
|
||||
name: 'portainer.newstack',
|
||||
url: '/newstack',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/stacks/create/createstack.html',
|
||||
@@ -318,6 +318,17 @@ angular.module('portainer.app', [])
|
||||
}
|
||||
};
|
||||
|
||||
var deploykeys = {
|
||||
name: 'portainer.deploykeys',
|
||||
url: '/deploykeys',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/deploykeys/deploykeys.html',
|
||||
controller: 'DeploykeysController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var updatePassword = {
|
||||
name: 'portainer.updatePassword',
|
||||
url: '/update-password',
|
||||
@@ -434,6 +445,7 @@ angular.module('portainer.app', [])
|
||||
$stateRegistryProvider.register(stack);
|
||||
$stateRegistryProvider.register(stackCreation);
|
||||
$stateRegistryProvider.register(support);
|
||||
$stateRegistryProvider.register(deploykeys);
|
||||
$stateRegistryProvider.register(tags);
|
||||
$stateRegistryProvider.register(updatePassword);
|
||||
$stateRegistryProvider.register(users);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
angular.module('portainer.app').component('porStackauthControlForm', {
|
||||
templateUrl: 'app/portainer/components/StackAuthControlForm/porStackauthControlForm.html',
|
||||
controller: 'porStackauthControlFormController',
|
||||
bindings: {
|
||||
// This object will be populated with the form data.
|
||||
// Model reference in porStackauthControlFromModel.js
|
||||
formData: '=',
|
||||
// Optional. An existing resource control object that will be used to set
|
||||
// the default values of the component.
|
||||
resourceControl: '<'
|
||||
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
<div style="margin-bottom: 0">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-3">
|
||||
<label for="stack_sshrepository_path" class="col-sm-12 control-label text-left">Select deploy key</label>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="col-sm-12">
|
||||
<span ng-class="{ 'pull-right': $ctrl.formData.showAddAction }" style="width: 25%;">
|
||||
<ui-select ng-model="$ctrl.formData.GenrateSshkey.selected" on-select="$ctrl.formData.change($select.selected.Publickeypath)">
|
||||
<ui-select-match placeholder="Select a deploy key"> <!-- allow-clear="true" -->
|
||||
<span>{{ $select.selected.Name }}</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="category as category in ($ctrl.formData.GenrateSshkey | filter: $select.search)" >
|
||||
<span>{{ category.Name }}</span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</span>
|
||||
<input style="display: none;" id="selPKey" value="{{$ctrl.formData.GenrateSshkey.selected.Publickeypath}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="col-sm-5">
|
||||
<button disabled type="button" class="btn btn-primary btn-sm copyToClip" ng-click="$ctrl.formData.copytoclipboard($ctrl.formData.GenrateSshkey.selected)">
|
||||
<i class="fas fa-key space-right" aria-hidden="true"></i>Copy to clipboard
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<span>
|
||||
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<button type="button" class="btn btn-primary btn-sm" disabled id="btngeneratekey" ng-click="$ctrl.formData.createNewkey()">
|
||||
<i class="fas fa-key space-right" aria-hidden="true"></i>Generate a new deploy key
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p id="warningStackname" style="margin-left: 10px; float: left;"><i class="fa fa-exclamation-triangle text-warning"></i> Please specify a stack name.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
angular.module('portainer.app')
|
||||
.controller('porStackauthControlFormController', ['$q', '$state','$scope','DeploykeyService','StackService', 'UserService', 'TeamService', 'Notifications', 'Authentication', 'ResourceControlService',
|
||||
function ($q,$state,$scope, DeploykeyService, StackService, UserService, TeamService, Notifications, Authentication, ResourceControlService) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.formData.existingsshKey = [];
|
||||
ctrl.formData.newgenratedeploykey = [];
|
||||
ctrl.enableGenratekey = false;
|
||||
|
||||
ctrl.formData.copytoclipboard = function (){
|
||||
var $temp = $("<input>");
|
||||
$("body").append($temp);
|
||||
$temp.val($('#selPKey').val()).select();
|
||||
document.execCommand("copy");
|
||||
$temp.remove();
|
||||
$('#refreshRateChange').show();
|
||||
$('#refreshRateChange').fadeOut(2000);
|
||||
|
||||
}
|
||||
|
||||
ctrl.formData.change = function(key){
|
||||
if(key != ''){
|
||||
$('.copyToClip').removeAttr("disabled")
|
||||
}
|
||||
else{
|
||||
$('.copyToClip').attr('disabled','disabled');
|
||||
}
|
||||
}
|
||||
|
||||
ctrl.formData.createNewkey = function() {
|
||||
if(StackService.getStackName() == ''){
|
||||
$('#stack_name').focus();
|
||||
} else {
|
||||
var createKeyName = 'deploykey_' + StackService.getStackName();
|
||||
var userID = Authentication.getUserDetails().ID;
|
||||
DeploykeyService.createNewdeploykey(createKeyName,userID)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Key successfully created', createKeyName);
|
||||
//$state.reload();
|
||||
getAlldeploykeydata(data)
|
||||
$('.copyToClip').removeAttr("disabled")
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create key');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initComponent() {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1 ? true: false;
|
||||
ctrl.isAdmin = isAdmin;
|
||||
|
||||
if (isAdmin) {
|
||||
ctrl.formData.Ownership = 'administrators';
|
||||
}
|
||||
|
||||
/*Enable / Disable deploykey button based on text entering in stack name textbox*/
|
||||
if($('#stack_name').val() !=""){
|
||||
$('#btngeneratekey').removeAttr("disabled")
|
||||
$('#warningStackname').hide();
|
||||
} else {
|
||||
$('#btngeneratekey').attr('disabled','disabled');
|
||||
$('#warningStackname').show();
|
||||
}
|
||||
|
||||
DeploykeyService.deploykeys()
|
||||
.then(function success(data) {
|
||||
ctrl.formData.GenrateSshkey = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve keys');
|
||||
ctrl.formData.GenrateSshkey = [];
|
||||
});
|
||||
}
|
||||
|
||||
function getAlldeploykeydata(getdata){
|
||||
DeploykeyService.deploykeys()
|
||||
.then(function success(data) {
|
||||
ctrl.formData.GenrateSshkey = data;
|
||||
ctrl.formData.GenrateSshkey.selected = getdata;//{value : ctrl.formData.GenrateSshkey[data.length - 1]}
|
||||
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve keys');
|
||||
ctrl.formData.GenrateSshkey = [];
|
||||
});
|
||||
}
|
||||
|
||||
initComponent();
|
||||
}]);
|
||||
@@ -0,0 +1,6 @@
|
||||
function StackAuthControlFormData() {
|
||||
this.StackAuthControlEnabled = true;
|
||||
this.Ownership = 'private';
|
||||
this.ExistingsshKey = [];
|
||||
this.GenrateSshkey = [];
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fas fa-key" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>Action</a>
|
||||
</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 class="col-sm-3">
|
||||
<span class="md-checkbox">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
{{ item.Name }}
|
||||
</td>
|
||||
<td class="col-sm-3">
|
||||
<input style="display: none;" type="text" id="pubkey{{ $index }}" class="form-control" ng-model="rootFolders" ng-init="rootFolders='{{ item.Publickeypath }}'" value="{{ item.Publickeypath }}"/>
|
||||
<button type="button" class="btn btn-sm btn-information" ng-click = "$ctrl.copyToClipboard('pubkey'+$index, $index)">
|
||||
<i class="fas fa-key space-right" aria-hidden="true"></i>Copy to clipboard
|
||||
</button>
|
||||
<span>
|
||||
<i id="refreshRateChange{{ $index }}" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="1" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="1" class="text-center text-muted">No key available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||
</div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
angular.module('portainer.app').component('deploykeysDatatable', {
|
||||
templateUrl: 'app/portainer/components/datatables/deploykeys-datatable/deploykeysDatatable.html',
|
||||
controller: 'deploykeysDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
angular.module('portainer.app')
|
||||
.controller('deploykeysDatatableController', ['PaginationService', 'DatatableService',
|
||||
function (PaginationService, DatatableService) {
|
||||
|
||||
this.state = {
|
||||
selectAll: false,
|
||||
orderBy: this.orderBy,
|
||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
||||
displayTextFilter: false,
|
||||
selectedItemCount: 0,
|
||||
selectedItems: []
|
||||
};
|
||||
|
||||
this.changeOrderBy = function(orderField) {
|
||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||
this.state.orderBy = orderField;
|
||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
||||
};
|
||||
|
||||
this.selectItem = function(item) {
|
||||
if (item.Checked) {
|
||||
this.state.selectedItemCount++;
|
||||
this.state.selectedItems.push(item);
|
||||
} else {
|
||||
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
|
||||
this.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
this.selectAll = function() {
|
||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
||||
var item = this.state.filteredDataSet[i];
|
||||
if (!(item.External && item.Type === 2) && item.Checked !== this.state.selectAll) {
|
||||
item.Checked = this.state.selectAll;
|
||||
this.selectItem(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.changePaginationLimit = function() {
|
||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
||||
};
|
||||
|
||||
this.$onInit = function() {
|
||||
setDefaults(this);
|
||||
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
this.state.orderBy = storedOrder.orderBy;
|
||||
}
|
||||
};
|
||||
|
||||
function setDefaults(ctrl) {
|
||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
||||
}
|
||||
|
||||
this.copyToClipboard = function(cpotyText, indexNo){
|
||||
var $temp = $("<input>");
|
||||
$("body").append($temp);
|
||||
$temp.val($('#'+cpotyText).val()).select();
|
||||
document.execCommand("copy");
|
||||
$temp.remove();
|
||||
$('#refreshRateChange'+indexNo).show();
|
||||
$('#refreshRateChange'+indexNo).fadeOut(2000);
|
||||
}
|
||||
}]);
|
||||
@@ -11,7 +11,7 @@
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.stacks.new">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.newstack">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
angular.module('portainer.app').controller('EndpointItemController', [
|
||||
function EndpointItemController() {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.editEndpoint = editEndpoint;
|
||||
|
||||
function editEndpoint(event) {
|
||||
event.stopPropagation();
|
||||
ctrl.onEdit(ctrl.model.Id);
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -21,8 +21,16 @@
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<span class="small">
|
||||
Group: {{ $ctrl.model.GroupName }}
|
||||
<span>
|
||||
|
||||
<span class="small">
|
||||
Group: {{ $ctrl.model.GroupName }}
|
||||
</span>
|
||||
<button
|
||||
ng-if="$ctrl.isAdmin"
|
||||
class="btn btn-link btn-xs"
|
||||
ng-click="$ctrl.editEndpoint($event)"><i class="fa fa-pencil-alt"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ angular.module('portainer.app').component('endpointItem', {
|
||||
templateUrl: 'app/portainer/components/endpoint-list/endpoint-item/endpointItem.html',
|
||||
bindings: {
|
||||
model: '<',
|
||||
onSelect: '<'
|
||||
}
|
||||
onSelect: '<',
|
||||
onEdit: '<',
|
||||
isAdmin:'<'
|
||||
},
|
||||
controller: 'EndpointItemController'
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
angular.module('portainer.app').controller('EndpointListController', [
|
||||
function EndpointListController() {
|
||||
var ctrl = this;
|
||||
ctrl.state = {
|
||||
textFilter: '',
|
||||
filteredEndpoints: []
|
||||
};
|
||||
|
||||
ctrl.$onChanges = $onChanges;
|
||||
ctrl.onFilterChanged = onFilterChanged;
|
||||
|
||||
function $onChanges(changesObj) {
|
||||
handleEndpointsChange(changesObj.endpoints);
|
||||
}
|
||||
|
||||
function handleEndpointsChange(endpoints) {
|
||||
if (!endpoints) {
|
||||
return;
|
||||
}
|
||||
if (!endpoints.currentValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
onFilterChanged();
|
||||
}
|
||||
|
||||
function onFilterChanged() {
|
||||
var filterValue = ctrl.state.textFilter;
|
||||
ctrl.state.filteredEndpoints = filterEndpoints(
|
||||
ctrl.endpoints,
|
||||
filterValue
|
||||
);
|
||||
}
|
||||
|
||||
function filterEndpoints(endpoints, filterValue) {
|
||||
if (!endpoints || !endpoints.length || !filterValue) {
|
||||
return endpoints;
|
||||
}
|
||||
var keywords = filterValue.split(' ');
|
||||
return _.filter(endpoints, function(endpoint) {
|
||||
var statusString = convertStatusToString(endpoint.Status);
|
||||
return _.every(keywords, function(keyword) {
|
||||
var lowerCaseKeyword = keyword.toLowerCase();
|
||||
return (
|
||||
_.includes(endpoint.Name.toLowerCase(), lowerCaseKeyword) ||
|
||||
_.includes(endpoint.GroupName.toLowerCase(), lowerCaseKeyword) ||
|
||||
_.some(endpoint.Tags, function(tag) {
|
||||
return _.includes(tag.toLowerCase(), lowerCaseKeyword);
|
||||
}) ||
|
||||
_.includes(statusString, keyword)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function convertStatusToString(status) {
|
||||
return status === 1 ? 'up' : 'down';
|
||||
}
|
||||
}
|
||||
]);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user