Merge branch 'master' into f-tls-parse-certs
This commit is contained in:
commit
446fc64850
|
@ -3,6 +3,7 @@
|
|||
IMPROVEMENTS:
|
||||
* core: Updated serf library to improve how leave intents are handled [[GH-4278](https://github.com/hashicorp/nomad/issues/4278)]
|
||||
* core: Add more descriptive errors when parsing agent TLS certificates [[GH-4340](https://github.com/hashicorp/nomad/issues/4340)]
|
||||
* core: Added TLS configuration option to prefer server's ciphersuites over clients[[GH-4338](https://github.com/hashicorp/nomad/issues/4338)]
|
||||
* core: Add the option for operators to configure TLS versions and allowed
|
||||
cipher suites. Default is a subset of safe ciphers and TLS 1.2 [[GH-4269](https://github.com/hashicorp/nomad/pull/4269)]
|
||||
* core: Add a new [progress_deadline](https://www.nomadproject.io/docs/job-specification/update.html#progress_deadline) parameter to
|
||||
|
|
|
@ -162,8 +162,8 @@ type UniversalExecutor struct {
|
|||
processExited chan interface{}
|
||||
fsIsolationEnforced bool
|
||||
|
||||
lre *logging.FileRotator
|
||||
lro *logging.FileRotator
|
||||
lre *logRotatorWrapper
|
||||
lro *logRotatorWrapper
|
||||
rotatorLock sync.Mutex
|
||||
|
||||
syslogServer *logging.SyslogServer
|
||||
|
@ -252,8 +252,8 @@ func (e *UniversalExecutor) LaunchCmd(command *ExecCommand) (*ProcessState, erro
|
|||
if err := e.configureLoggers(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.cmd.Stdout = e.lro
|
||||
e.cmd.Stderr = e.lre
|
||||
e.cmd.Stdout = e.lro.processOutWriter
|
||||
e.cmd.Stderr = e.lre.processOutWriter
|
||||
|
||||
// Look up the binary path and make it executable
|
||||
absPath, err := e.lookupBin(e.ctx.TaskEnv.ReplaceEnv(command.Cmd))
|
||||
|
@ -348,7 +348,12 @@ func (e *UniversalExecutor) configureLoggers() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("error creating new stdout log file for %q: %v", e.ctx.Task.Name, err)
|
||||
}
|
||||
e.lro = lro
|
||||
|
||||
r, err := NewLogRotatorWrapper(lro)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.lro = r
|
||||
}
|
||||
|
||||
if e.lre == nil {
|
||||
|
@ -357,7 +362,12 @@ func (e *UniversalExecutor) configureLoggers() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("error creating new stderr log file for %q: %v", e.ctx.Task.Name, err)
|
||||
}
|
||||
e.lre = lre
|
||||
|
||||
r, err := NewLogRotatorWrapper(lre)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.lre = r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -375,14 +385,14 @@ func (e *UniversalExecutor) UpdateLogConfig(logConfig *structs.LogConfig) error
|
|||
if e.lro == nil {
|
||||
return fmt.Errorf("log rotator for stdout doesn't exist")
|
||||
}
|
||||
e.lro.MaxFiles = logConfig.MaxFiles
|
||||
e.lro.FileSize = int64(logConfig.MaxFileSizeMB * 1024 * 1024)
|
||||
e.lro.rotatorWriter.MaxFiles = logConfig.MaxFiles
|
||||
e.lro.rotatorWriter.FileSize = int64(logConfig.MaxFileSizeMB * 1024 * 1024)
|
||||
|
||||
if e.lre == nil {
|
||||
return fmt.Errorf("log rotator for stderr doesn't exist")
|
||||
}
|
||||
e.lre.MaxFiles = logConfig.MaxFiles
|
||||
e.lre.FileSize = int64(logConfig.MaxFileSizeMB * 1024 * 1024)
|
||||
e.lre.rotatorWriter.MaxFiles = logConfig.MaxFiles
|
||||
e.lre.rotatorWriter.FileSize = int64(logConfig.MaxFileSizeMB * 1024 * 1024)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -393,10 +403,10 @@ func (e *UniversalExecutor) UpdateTask(task *structs.Task) error {
|
|||
e.rotatorLock.Lock()
|
||||
if e.lro != nil && e.lre != nil {
|
||||
fileSize := int64(task.LogConfig.MaxFileSizeMB * 1024 * 1024)
|
||||
e.lro.MaxFiles = task.LogConfig.MaxFiles
|
||||
e.lro.FileSize = fileSize
|
||||
e.lre.MaxFiles = task.LogConfig.MaxFiles
|
||||
e.lre.FileSize = fileSize
|
||||
e.lro.rotatorWriter.MaxFiles = task.LogConfig.MaxFiles
|
||||
e.lro.rotatorWriter.FileSize = fileSize
|
||||
e.lre.rotatorWriter.MaxFiles = task.LogConfig.MaxFiles
|
||||
e.lre.rotatorWriter.FileSize = fileSize
|
||||
}
|
||||
e.rotatorLock.Unlock()
|
||||
return nil
|
||||
|
@ -799,7 +809,7 @@ func (e *UniversalExecutor) LaunchSyslogServer() (*SyslogServerState, error) {
|
|||
|
||||
e.syslogServer = logging.NewSyslogServer(l, e.syslogChan, e.logger)
|
||||
go e.syslogServer.Start()
|
||||
go e.collectLogs(e.lre, e.lro)
|
||||
go e.collectLogs(e.lre.rotatorWriter, e.lro.rotatorWriter)
|
||||
syslogAddr := fmt.Sprintf("%s://%s", l.Addr().Network(), l.Addr().String())
|
||||
return &SyslogServerState{Addr: syslogAddr}, nil
|
||||
}
|
||||
|
@ -809,11 +819,54 @@ func (e *UniversalExecutor) collectLogs(we io.Writer, wo io.Writer) {
|
|||
// If the severity of the log line is err then we write to stderr
|
||||
// otherwise all messages go to stdout
|
||||
if logParts.Severity == syslog.LOG_ERR {
|
||||
e.lre.Write(logParts.Message)
|
||||
e.lre.Write([]byte{'\n'})
|
||||
we.Write(logParts.Message)
|
||||
we.Write([]byte{'\n'})
|
||||
} else {
|
||||
e.lro.Write(logParts.Message)
|
||||
e.lro.Write([]byte{'\n'})
|
||||
wo.Write(logParts.Message)
|
||||
wo.Write([]byte{'\n'})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logRotatorWrapper wraps our log rotator and exposes a pipe that can feed the
|
||||
// log rotator data. The processOutWriter should be attached to the process and
|
||||
// data will be copied from the reader to the rotator.
|
||||
type logRotatorWrapper struct {
|
||||
processOutWriter *os.File
|
||||
processOutReader *os.File
|
||||
rotatorWriter *logging.FileRotator
|
||||
}
|
||||
|
||||
// NewLogRotatorWrapper takes a rotator and returns a wrapper that has the
|
||||
// processOutWriter to attach to the processes stdout or stderr.
|
||||
func NewLogRotatorWrapper(rotator *logging.FileRotator) (*logRotatorWrapper, error) {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create os.Pipe for extracting logs: %v", err)
|
||||
}
|
||||
|
||||
wrap := &logRotatorWrapper{
|
||||
processOutWriter: w,
|
||||
processOutReader: r,
|
||||
rotatorWriter: rotator,
|
||||
}
|
||||
wrap.start()
|
||||
return wrap, nil
|
||||
}
|
||||
|
||||
// start starts a go-routine that copies from the pipe into the rotator. This is
|
||||
// called by the constructor and not the user of the wrapper.
|
||||
func (l *logRotatorWrapper) start() {
|
||||
go func() {
|
||||
io.Copy(l.rotatorWriter, l.processOutReader)
|
||||
l.processOutReader.Close() // in case io.Copy stopped due to write error
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes the rotator and the process writer to ensure that the Wait
|
||||
// command exits.
|
||||
func (l *logRotatorWrapper) Close() error {
|
||||
l.rotatorWriter.Close()
|
||||
return l.processOutWriter.Close()
|
||||
}
|
||||
|
|
|
@ -156,6 +156,9 @@ tls {
|
|||
key_file = "pipe"
|
||||
rpc_upgrade_mode = true
|
||||
verify_https_client = true
|
||||
tls_prefer_server_cipher_suites = true
|
||||
tls_cipher_suites = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
||||
tls_min_version = "tls12"
|
||||
}
|
||||
sentinel {
|
||||
import "foo" {
|
||||
|
|
|
@ -763,6 +763,7 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error {
|
|||
"verify_https_client",
|
||||
"tls_cipher_suites",
|
||||
"tls_min_version",
|
||||
"tls_prefer_server_cipher_suites",
|
||||
}
|
||||
|
||||
if err := helper.CheckHCLKeys(listVal, valid); err != nil {
|
||||
|
|
|
@ -167,14 +167,17 @@ func TestConfig_Parse(t *testing.T) {
|
|||
Token: "12345",
|
||||
},
|
||||
TLSConfig: &config.TLSConfig{
|
||||
EnableHTTP: true,
|
||||
EnableRPC: true,
|
||||
VerifyServerHostname: true,
|
||||
CAFile: "foo",
|
||||
CertFile: "bar",
|
||||
KeyFile: "pipe",
|
||||
RPCUpgradeMode: true,
|
||||
VerifyHTTPSClient: true,
|
||||
EnableHTTP: true,
|
||||
EnableRPC: true,
|
||||
VerifyServerHostname: true,
|
||||
CAFile: "foo",
|
||||
CertFile: "bar",
|
||||
KeyFile: "pipe",
|
||||
RPCUpgradeMode: true,
|
||||
VerifyHTTPSClient: true,
|
||||
TLSPreferServerCipherSuites: true,
|
||||
TLSCipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
TLSMinVersion: "tls12",
|
||||
},
|
||||
HTTPAPIResponseHeaders: map[string]string{
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
|
|
|
@ -106,6 +106,12 @@ type Config struct {
|
|||
// these values for acceptable safe alternatives.
|
||||
CipherSuites []uint16
|
||||
|
||||
// PreferServerCipherSuites controls whether the server selects the
|
||||
// client's most preferred ciphersuite, or the server's most preferred
|
||||
// ciphersuite. If true then the server's preference, as expressed in
|
||||
// the order of elements in CipherSuites, is used.
|
||||
PreferServerCipherSuites bool
|
||||
|
||||
// MinVersion contains the minimum SSL/TLS version that is accepted.
|
||||
MinVersion uint16
|
||||
}
|
||||
|
@ -122,15 +128,16 @@ func NewTLSConfiguration(newConf *config.TLSConfig, verifyIncoming, verifyOutgoi
|
|||
}
|
||||
|
||||
return &Config{
|
||||
VerifyIncoming: verifyIncoming,
|
||||
VerifyOutgoing: verifyOutgoing,
|
||||
VerifyServerHostname: newConf.VerifyServerHostname,
|
||||
CAFile: newConf.CAFile,
|
||||
CertFile: newConf.CertFile,
|
||||
KeyFile: newConf.KeyFile,
|
||||
KeyLoader: newConf.GetKeyLoader(),
|
||||
CipherSuites: ciphers,
|
||||
MinVersion: minVersion,
|
||||
VerifyIncoming: verifyIncoming,
|
||||
VerifyOutgoing: verifyOutgoing,
|
||||
VerifyServerHostname: newConf.VerifyServerHostname,
|
||||
CAFile: newConf.CAFile,
|
||||
CertFile: newConf.CertFile,
|
||||
KeyFile: newConf.KeyFile,
|
||||
KeyLoader: newConf.GetKeyLoader(),
|
||||
CipherSuites: ciphers,
|
||||
MinVersion: minVersion,
|
||||
PreferServerCipherSuites: newConf.TLSPreferServerCipherSuites,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -212,10 +219,11 @@ func (c *Config) OutgoingTLSConfig() (*tls.Config, error) {
|
|||
}
|
||||
// Create the tlsConfig
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: x509.NewCertPool(),
|
||||
InsecureSkipVerify: true,
|
||||
CipherSuites: c.CipherSuites,
|
||||
MinVersion: c.MinVersion,
|
||||
RootCAs: x509.NewCertPool(),
|
||||
InsecureSkipVerify: true,
|
||||
CipherSuites: c.CipherSuites,
|
||||
MinVersion: c.MinVersion,
|
||||
PreferServerCipherSuites: c.PreferServerCipherSuites,
|
||||
}
|
||||
if c.VerifyServerHostname {
|
||||
tlsConfig.InsecureSkipVerify = false
|
||||
|
@ -332,10 +340,11 @@ func WrapTLSClient(conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) {
|
|||
func (c *Config) IncomingTLSConfig() (*tls.Config, error) {
|
||||
// Create the tlsConfig
|
||||
tlsConfig := &tls.Config{
|
||||
ClientCAs: x509.NewCertPool(),
|
||||
ClientAuth: tls.NoClientCert,
|
||||
CipherSuites: c.CipherSuites,
|
||||
MinVersion: c.MinVersion,
|
||||
ClientCAs: x509.NewCertPool(),
|
||||
ClientAuth: tls.NoClientCert,
|
||||
CipherSuites: c.CipherSuites,
|
||||
MinVersion: c.MinVersion,
|
||||
PreferServerCipherSuites: c.PreferServerCipherSuites,
|
||||
}
|
||||
|
||||
// Parse the CA cert if any
|
||||
|
|
|
@ -296,6 +296,60 @@ func TestConfig_OutgoingTLS_WithKeyPair(t *testing.T) {
|
|||
assert.NotNil(cert)
|
||||
}
|
||||
|
||||
func TestConfig_OutgoingTLS_PreferServerCipherSuites(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
{
|
||||
conf := &Config{
|
||||
VerifyOutgoing: true,
|
||||
CAFile: cacert,
|
||||
}
|
||||
tlsConfig, err := conf.OutgoingTLSConfig()
|
||||
require.Nil(err)
|
||||
require.Equal(tlsConfig.PreferServerCipherSuites, false)
|
||||
}
|
||||
{
|
||||
conf := &Config{
|
||||
VerifyOutgoing: true,
|
||||
CAFile: cacert,
|
||||
PreferServerCipherSuites: true,
|
||||
}
|
||||
tlsConfig, err := conf.OutgoingTLSConfig()
|
||||
require.Nil(err)
|
||||
require.Equal(tlsConfig.PreferServerCipherSuites, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_OutgoingTLS_TLSCipherSuites(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
{
|
||||
defaultCiphers := []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
}
|
||||
conf := &Config{
|
||||
VerifyOutgoing: true,
|
||||
CAFile: cacert,
|
||||
CipherSuites: defaultCiphers,
|
||||
}
|
||||
tlsConfig, err := conf.OutgoingTLSConfig()
|
||||
require.Nil(err)
|
||||
require.Equal(tlsConfig.CipherSuites, defaultCiphers)
|
||||
}
|
||||
{
|
||||
conf := &Config{
|
||||
VerifyOutgoing: true,
|
||||
CAFile: cacert,
|
||||
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
|
||||
}
|
||||
tlsConfig, err := conf.OutgoingTLSConfig()
|
||||
require.Nil(err)
|
||||
require.Equal(tlsConfig.CipherSuites, []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305})
|
||||
}
|
||||
}
|
||||
|
||||
func startTLSServer(config *Config) (net.Conn, chan error) {
|
||||
errc := make(chan error, 1)
|
||||
|
||||
|
@ -545,6 +599,51 @@ func TestConfig_IncomingTLS_NoVerify(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConfig_IncomingTLS_PreferServerCipherSuites(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
{
|
||||
conf := &Config{}
|
||||
tlsConfig, err := conf.IncomingTLSConfig()
|
||||
require.Nil(err)
|
||||
require.Equal(tlsConfig.PreferServerCipherSuites, false)
|
||||
}
|
||||
{
|
||||
conf := &Config{
|
||||
PreferServerCipherSuites: true,
|
||||
}
|
||||
tlsConfig, err := conf.IncomingTLSConfig()
|
||||
require.Nil(err)
|
||||
require.Equal(tlsConfig.PreferServerCipherSuites, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_IncomingTLS_TLSCipherSuites(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
{
|
||||
defaultCiphers := []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
}
|
||||
conf := &Config{
|
||||
CipherSuites: defaultCiphers,
|
||||
}
|
||||
tlsConfig, err := conf.IncomingTLSConfig()
|
||||
require.Nil(err)
|
||||
require.Equal(tlsConfig.CipherSuites, defaultCiphers)
|
||||
}
|
||||
{
|
||||
conf := &Config{
|
||||
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
|
||||
}
|
||||
tlsConfig, err := conf.IncomingTLSConfig()
|
||||
require.Nil(err)
|
||||
require.Equal(tlsConfig.CipherSuites, []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_ParseCiphers_Valid(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
|
|
|
@ -63,6 +63,12 @@ type TLSConfig struct {
|
|||
// TLSMinVersion is used to set the minimum TLS version used for TLS
|
||||
// connections. Should be either "tls10", "tls11", or "tls12".
|
||||
TLSMinVersion string `mapstructure:"tls_min_version"`
|
||||
|
||||
// TLSPreferServerCipherSuites controls whether the server selects the
|
||||
// client's most preferred ciphersuite, or the server's most preferred
|
||||
// ciphersuite. If true then the server's preference, as expressed in
|
||||
// the order of elements in CipherSuites, is used.
|
||||
TLSPreferServerCipherSuites bool `mapstructure:"tls_prefer_server_cipher_suites"`
|
||||
}
|
||||
|
||||
type KeyLoader struct {
|
||||
|
@ -158,6 +164,8 @@ func (t *TLSConfig) Copy() *TLSConfig {
|
|||
new.TLSCipherSuites = t.TLSCipherSuites
|
||||
new.TLSMinVersion = t.TLSMinVersion
|
||||
|
||||
new.TLSPreferServerCipherSuites = t.TLSPreferServerCipherSuites
|
||||
|
||||
new.SetChecksum()
|
||||
|
||||
return new
|
||||
|
@ -211,6 +219,9 @@ func (t *TLSConfig) Merge(b *TLSConfig) *TLSConfig {
|
|||
if b.TLSMinVersion != "" {
|
||||
result.TLSMinVersion = b.TLSMinVersion
|
||||
}
|
||||
if b.TLSPreferServerCipherSuites {
|
||||
result.TLSPreferServerCipherSuites = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
|
@ -15,14 +15,15 @@ func TestTLSConfig_Merge(t *testing.T) {
|
|||
}
|
||||
|
||||
b := &TLSConfig{
|
||||
EnableHTTP: true,
|
||||
EnableRPC: true,
|
||||
VerifyServerHostname: true,
|
||||
CAFile: "test-ca-file-2",
|
||||
CertFile: "test-cert-file-2",
|
||||
RPCUpgradeMode: true,
|
||||
TLSCipherSuites: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
TLSMinVersion: "tls12",
|
||||
EnableHTTP: true,
|
||||
EnableRPC: true,
|
||||
VerifyServerHostname: true,
|
||||
CAFile: "test-ca-file-2",
|
||||
CertFile: "test-cert-file-2",
|
||||
RPCUpgradeMode: true,
|
||||
TLSCipherSuites: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
TLSMinVersion: "tls12",
|
||||
TLSPreferServerCipherSuites: true,
|
||||
}
|
||||
|
||||
new := a.Merge(b)
|
||||
|
@ -173,10 +174,12 @@ func TestTLS_Copy(t *testing.T) {
|
|||
fookey = "../../../helper/tlsutil/testdata/nomad-foo-key.pem"
|
||||
)
|
||||
a := &TLSConfig{
|
||||
CAFile: cafile,
|
||||
CertFile: foocert,
|
||||
KeyFile: fookey,
|
||||
TLSCipherSuites: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
CAFile: cafile,
|
||||
CertFile: foocert,
|
||||
KeyFile: fookey,
|
||||
TLSCipherSuites: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
TLSMinVersion: "tls12",
|
||||
TLSPreferServerCipherSuites: true,
|
||||
}
|
||||
a.SetChecksum()
|
||||
|
||||
|
|
|
@ -47,11 +47,12 @@ export default Watchable.extend({
|
|||
},
|
||||
|
||||
xhrKey(url, method, options = {}) {
|
||||
const plainKey = this._super(...arguments);
|
||||
const namespace = options.data && options.data.namespace;
|
||||
if (namespace) {
|
||||
return `${url}?namespace=${namespace}`;
|
||||
return `${plainKey}?namespace=${namespace}`;
|
||||
}
|
||||
return url;
|
||||
return plainKey;
|
||||
},
|
||||
|
||||
relationshipFallbackLinks: {
|
||||
|
|
|
@ -51,8 +51,8 @@ export default ApplicationAdapter.extend({
|
|||
return ajaxOptions;
|
||||
},
|
||||
|
||||
xhrKey(url /* method, options */) {
|
||||
return url;
|
||||
xhrKey(url, method /* options */) {
|
||||
return `${method} ${url}`;
|
||||
},
|
||||
|
||||
findAll(store, type, sinceToken, snapshotRecordArray, additionalParams = {}) {
|
||||
|
@ -149,7 +149,7 @@ export default ApplicationAdapter.extend({
|
|||
return;
|
||||
}
|
||||
const url = this.urlForFindRecord(id, modelName);
|
||||
this.get('xhrs').cancel(url);
|
||||
this.get('xhrs').cancel(`GET ${url}`);
|
||||
},
|
||||
|
||||
cancelFindAll(modelName) {
|
||||
|
@ -161,7 +161,7 @@ export default ApplicationAdapter.extend({
|
|||
if (params) {
|
||||
url = `${url}?${params}`;
|
||||
}
|
||||
this.get('xhrs').cancel(url);
|
||||
this.get('xhrs').cancel(`GET ${url}`);
|
||||
},
|
||||
|
||||
cancelReloadRelationship(model, relationshipName) {
|
||||
|
@ -175,7 +175,7 @@ export default ApplicationAdapter.extend({
|
|||
);
|
||||
} else {
|
||||
const url = model[relationship.kind](relationship.key).link();
|
||||
this.get('xhrs').cancel(url);
|
||||
this.get('xhrs').cancel(`GET ${url}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed, get } from '@ember/object';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['accordion'],
|
||||
|
||||
key: 'id',
|
||||
source: computed(() => []),
|
||||
|
||||
decoratedSource: computed('source.[]', function() {
|
||||
const stateCache = this.get('stateCache');
|
||||
const key = this.get('key');
|
||||
const deepKey = `item.${key}`;
|
||||
|
||||
const decoratedSource = this.get('source').map(item => {
|
||||
const cacheItem = stateCache.findBy(deepKey, get(item, key));
|
||||
return {
|
||||
item,
|
||||
isOpen: cacheItem ? !!cacheItem.isOpen : false,
|
||||
};
|
||||
});
|
||||
|
||||
this.set('stateCache', decoratedSource);
|
||||
return decoratedSource;
|
||||
}),
|
||||
|
||||
// When source updates come in, the state cache is used to preserve
|
||||
// open/close state.
|
||||
stateCache: computed(() => []),
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
isOpen: false,
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['accordion-head'],
|
||||
classNameBindings: ['isOpen::is-light', 'isExpandable::is-inactive'],
|
||||
|
||||
'data-test-accordion-head': true,
|
||||
|
||||
buttonLabel: 'toggle',
|
||||
isOpen: false,
|
||||
isExpandable: true,
|
||||
item: null,
|
||||
|
||||
onClose() {},
|
||||
onOpen() {},
|
||||
});
|
|
@ -24,6 +24,16 @@ export default Controller.extend(Sortable, Searchable, {
|
|||
listToSearch: alias('listSorted'),
|
||||
sortedAllocations: alias('listSearched'),
|
||||
|
||||
sortedEvents: computed('model.events.@each.time', function() {
|
||||
return this.get('model.events')
|
||||
.sortBy('time')
|
||||
.reverse();
|
||||
}),
|
||||
|
||||
sortedDrivers: computed('model.drivers.@each.name', function() {
|
||||
return this.get('model.drivers').sortBy('name');
|
||||
}),
|
||||
|
||||
actions: {
|
||||
gotoAllocation(allocation) {
|
||||
this.transitionToRoute('allocations.allocation', allocation);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import Ember from 'ember';
|
||||
import Mixin from '@ember/object/mixin';
|
||||
import { assert } from '@ember/debug';
|
||||
|
||||
|
@ -7,11 +8,15 @@ export default Mixin.create({
|
|||
},
|
||||
|
||||
setupDocumentVisibility: function() {
|
||||
this.set('_visibilityHandler', this.get('visibilityHandler').bind(this));
|
||||
document.addEventListener('visibilitychange', this.get('_visibilityHandler'));
|
||||
if (!Ember.testing) {
|
||||
this.set('_visibilityHandler', this.get('visibilityHandler').bind(this));
|
||||
document.addEventListener('visibilitychange', this.get('_visibilityHandler'));
|
||||
}
|
||||
}.on('init'),
|
||||
|
||||
removeDocumentVisibility: function() {
|
||||
document.removeEventListener('visibilitychange', this.get('_visibilityHandler'));
|
||||
if (!Ember.testing) {
|
||||
document.removeEventListener('visibilitychange', this.get('_visibilityHandler'));
|
||||
}
|
||||
}.on('willDestroy'),
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import Ember from 'ember';
|
||||
import Mixin from '@ember/object/mixin';
|
||||
import { assert } from '@ember/debug';
|
||||
|
||||
|
@ -7,11 +8,15 @@ export default Mixin.create({
|
|||
},
|
||||
|
||||
setupDocumentVisibility: function() {
|
||||
this.set('_visibilityHandler', this.get('visibilityHandler').bind(this));
|
||||
document.addEventListener('visibilitychange', this.get('_visibilityHandler'));
|
||||
if (!Ember.testing) {
|
||||
this.set('_visibilityHandler', this.get('visibilityHandler').bind(this));
|
||||
document.addEventListener('visibilitychange', this.get('_visibilityHandler'));
|
||||
}
|
||||
}.on('activate'),
|
||||
|
||||
removeDocumentVisibility: function() {
|
||||
document.removeEventListener('visibilitychange', this.get('_visibilityHandler'));
|
||||
if (!Ember.testing) {
|
||||
document.removeEventListener('visibilitychange', this.get('_visibilityHandler'));
|
||||
}
|
||||
}.on('deactivate'),
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import Model from 'ember-data/model';
|
|||
import attr from 'ember-data/attr';
|
||||
import { belongsTo } from 'ember-data/relationships';
|
||||
import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes';
|
||||
import intersection from 'npm:lodash.intersection';
|
||||
import shortUUIDProperty from '../utils/properties/short-uuid';
|
||||
import AllocationStats from '../utils/classes/allocation-stats';
|
||||
|
||||
|
@ -61,6 +62,17 @@ export default Model.extend({
|
|||
return taskGroups && taskGroups.findBy('name', this.get('taskGroupName'));
|
||||
}),
|
||||
|
||||
unhealthyDrivers: computed('taskGroup.drivers.[]', 'node.unhealthyDriverNames.[]', function() {
|
||||
const taskGroupUnhealthyDrivers = this.get('taskGroup.drivers');
|
||||
const nodeUnhealthyDrivers = this.get('node.unhealthyDriverNames');
|
||||
|
||||
if (taskGroupUnhealthyDrivers && nodeUnhealthyDrivers) {
|
||||
return intersection(taskGroupUnhealthyDrivers, nodeUnhealthyDrivers);
|
||||
}
|
||||
|
||||
return [];
|
||||
}),
|
||||
|
||||
fetchStats() {
|
||||
return this.get('token')
|
||||
.authorizedRequest(`/v1/client/allocation/${this.get('id')}/stats`)
|
||||
|
|
|
@ -116,6 +116,28 @@ export default Model.extend({
|
|||
evaluations: hasMany('evaluations'),
|
||||
namespace: belongsTo('namespace'),
|
||||
|
||||
drivers: computed('taskGroups.@each.drivers', function() {
|
||||
return this.get('taskGroups')
|
||||
.mapBy('drivers')
|
||||
.reduce((all, drivers) => {
|
||||
all.push(...drivers);
|
||||
return all;
|
||||
}, [])
|
||||
.uniq();
|
||||
}),
|
||||
|
||||
// Getting all unhealthy drivers for a job can be incredibly expensive if the job
|
||||
// has many allocations. This can lead to making an API request for many nodes.
|
||||
unhealthyDrivers: computed('allocations.@each.unhealthyDrivers.[]', function() {
|
||||
return this.get('allocations')
|
||||
.mapBy('unhealthyDrivers')
|
||||
.reduce((all, drivers) => {
|
||||
all.push(...drivers);
|
||||
return all;
|
||||
}, [])
|
||||
.uniq();
|
||||
}),
|
||||
|
||||
hasBlockedEvaluation: computed('evaluations.@each.isBlocked', function() {
|
||||
return this.get('evaluations')
|
||||
.toArray()
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import { computed, get } from '@ember/object';
|
||||
import attr from 'ember-data/attr';
|
||||
import { fragmentOwner } from 'ember-data-model-fragments/attributes';
|
||||
import { fragment } from 'ember-data-model-fragments/attributes';
|
||||
|
||||
export default Fragment.extend({
|
||||
node: fragmentOwner(),
|
||||
|
||||
attributes: fragment('node-attributes'),
|
||||
|
||||
attributesShort: computed('name', 'attributes.attributesStructured', function() {
|
||||
const attributes = this.get('attributes.attributesStructured');
|
||||
return get(attributes, `driver.${this.get('name')}`);
|
||||
}),
|
||||
|
||||
name: attr('string'),
|
||||
detected: attr('boolean', { defaultValue: false }),
|
||||
healthy: attr('boolean', { defaultValue: false }),
|
||||
healthDescription: attr('string'),
|
||||
updateTime: attr('date'),
|
||||
|
||||
healthClass: computed('healthy', function() {
|
||||
return this.get('healthy') ? 'running' : 'failed';
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { alias } from '@ember/object/computed';
|
||||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import attr from 'ember-data/attr';
|
||||
import { fragmentOwner } from 'ember-data-model-fragments/attributes';
|
||||
|
||||
export default Fragment.extend({
|
||||
node: fragmentOwner(),
|
||||
|
||||
message: attr('string'),
|
||||
subsystem: attr('string'),
|
||||
details: attr(),
|
||||
time: attr('date'),
|
||||
|
||||
driver: alias('details.driver'),
|
||||
});
|
|
@ -2,7 +2,7 @@ import { computed } from '@ember/object';
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
import { hasMany } from 'ember-data/relationships';
|
||||
import { fragment } from 'ember-data-model-fragments/attributes';
|
||||
import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes';
|
||||
import shortUUIDProperty from '../utils/properties/short-uuid';
|
||||
import ipParts from '../utils/ip-parts';
|
||||
|
||||
|
@ -37,4 +37,19 @@ export default Model.extend({
|
|||
}),
|
||||
|
||||
allocations: hasMany('allocations', { inverse: 'node' }),
|
||||
|
||||
drivers: fragmentArray('node-driver'),
|
||||
events: fragmentArray('node-event'),
|
||||
|
||||
detectedDrivers: computed('drivers.@each.detected', function() {
|
||||
return this.get('drivers').filterBy('detected');
|
||||
}),
|
||||
|
||||
unhealthyDrivers: computed('detectedDrivers.@each.healthy', function() {
|
||||
return this.get('detectedDrivers').filterBy('healthy', false);
|
||||
}),
|
||||
|
||||
unhealthyDriverNames: computed('unhealthyDrivers.@each.name', function() {
|
||||
return this.get('unhealthyDrivers').mapBy('name');
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -14,6 +14,12 @@ export default Fragment.extend({
|
|||
|
||||
tasks: fragmentArray('task'),
|
||||
|
||||
drivers: computed('tasks.@each.driver', function() {
|
||||
return this.get('tasks')
|
||||
.mapBy('driver')
|
||||
.uniq();
|
||||
}),
|
||||
|
||||
allocations: computed('job.allocations.@each.taskGroup', function() {
|
||||
return maybe(this.get('job.allocations')).filterBy('taskGroupName', this.get('name'));
|
||||
}),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { none } from '@ember/object/computed';
|
||||
import { computed } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import attr from 'ember-data/attr';
|
||||
import { fragment, fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes';
|
||||
|
@ -19,6 +20,15 @@ export default Fragment.extend({
|
|||
return tasks && tasks.findBy('name', this.get('name'));
|
||||
}),
|
||||
|
||||
driver: alias('task.driver'),
|
||||
|
||||
// TaskState represents a task running on a node, so in addition to knowing the
|
||||
// driver via the task, the health of the driver is also known via the node
|
||||
driverStatus: computed('task.driver', 'allocation.node.drivers.[]', function() {
|
||||
const nodeDrivers = this.get('allocation.node.drivers') || [];
|
||||
return nodeDrivers.findBy('name', this.get('task.driver'));
|
||||
}),
|
||||
|
||||
resources: fragment('resources'),
|
||||
events: fragmentArray('task-event'),
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
attrs: {
|
||||
time: 'Timestamp',
|
||||
},
|
||||
});
|
|
@ -1,3 +1,5 @@
|
|||
import { get } from '@ember/object';
|
||||
import { assign } from '@ember/polyfills';
|
||||
import { inject as service } from '@ember/service';
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
|
@ -9,11 +11,10 @@ export default ApplicationSerializer.extend({
|
|||
},
|
||||
|
||||
normalize(modelClass, hash) {
|
||||
// Proxy local agent to the same proxy express server Ember is using
|
||||
// to avoid CORS
|
||||
if (this.get('config.isDev') && hash.HTTPAddr === '127.0.0.1:4646') {
|
||||
hash.HTTPAddr = '127.0.0.1:4200';
|
||||
}
|
||||
// Transform the map-based Drivers object into an array-based NodeDriver fragment list
|
||||
hash.Drivers = Object.keys(get(hash, 'Drivers') || {}).map(key => {
|
||||
return assign({}, get(hash, `Drivers.${key}`), { Name: key });
|
||||
});
|
||||
|
||||
return this._super(modelClass, hash);
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@import './components/accordion';
|
||||
@import './components/badge';
|
||||
@import './components/boxed-section';
|
||||
@import './components/cli-window';
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
.accordion {
|
||||
.accordion-head,
|
||||
.accordion-body {
|
||||
border: 1px solid $grey-blue;
|
||||
border-bottom: none;
|
||||
padding: 0.75em 1.5em;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: $radius;
|
||||
border-top-right-radius: $radius;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid $grey-blue;
|
||||
border-bottom-left-radius: $radius;
|
||||
border-bottom-right-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-head {
|
||||
display: flex;
|
||||
background: $white-ter;
|
||||
flex: 1;
|
||||
|
||||
&.is-light {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
&.is-inactive {
|
||||
color: $grey-light;
|
||||
}
|
||||
|
||||
.accordion-head-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.accordion-toggle {
|
||||
flex-basis: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,4 +33,13 @@
|
|||
&.is-faded {
|
||||
color: rgba($text, 0.8);
|
||||
}
|
||||
|
||||
&.is-small {
|
||||
padding: 0.15em 0.5em;
|
||||
}
|
||||
|
||||
&.is-secondary {
|
||||
color: darken($grey-blue, 30%);
|
||||
background: lighten($grey-blue, 10%);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,11 @@
|
|||
|
||||
&.is-narrow {
|
||||
padding: 1.25em 0 1.25em 0.5em;
|
||||
|
||||
& + th,
|
||||
& + td {
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
// Only use px modifiers when text needs to be truncated.
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
sortDescending=sortDescending
|
||||
class="is-striped" as |t|}}
|
||||
{{#t.head}}
|
||||
<th class="is-narrow"></th>
|
||||
{{#t.sort-by prop="name"}}Name{{/t.sort-by}}
|
||||
{{#t.sort-by prop="state"}}State{{/t.sort-by}}
|
||||
<th>Last Event</th>
|
||||
|
@ -48,6 +49,13 @@
|
|||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
<tr data-test-task-row={{row.model.task.name}}>
|
||||
<td class="is-narrow">
|
||||
{{#if (not row.model.driverStatus.healthy)}}
|
||||
<span data-test-icon="unhealthy-driver" class="tooltip text-center" aria-label="{{row.model.driver}} is unhealthy">
|
||||
{{x-icon "warning" class="is-warning"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-test-name>
|
||||
{{#link-to "allocations.allocation.task" row.model.allocation row.model}}
|
||||
{{row.model.name}}
|
||||
|
|
|
@ -17,9 +17,27 @@
|
|||
<div class="boxed-section is-small">
|
||||
<div class="boxed-section-body inline-definitions">
|
||||
<span class="label">Client Details</span>
|
||||
<span class="pair" data-test-status-definition><span class="term">Status</span> <span class="status-text node-{{model.status}}">{{model.status}}</span></span>
|
||||
<span class="pair" data-test-address-definition><span class="term">Address</span> {{model.httpAddr}}</span>
|
||||
<span class="pair" data-test-datacenter-definition><span class="term">Datacenter</span> {{model.datacenter}}</span>
|
||||
<span class="pair" data-test-status-definition>
|
||||
<span class="term">Status</span>
|
||||
<span class="status-text node-{{model.status}}">{{model.status}}</span>
|
||||
</span>
|
||||
<span class="pair" data-test-address-definition>
|
||||
<span class="term">Address</span>
|
||||
{{model.httpAddr}}
|
||||
</span>
|
||||
<span class="pair" data-test-datacenter-definition>
|
||||
<span class="term">Datacenter</span>
|
||||
{{model.datacenter}}
|
||||
</span>
|
||||
<span class="pair" data-test-driver-health>
|
||||
<span class="term">Drivers</span>
|
||||
{{#if model.unhealthyDrivers.length}}
|
||||
{{x-icon "warning" class="is-text is-warning"}}
|
||||
{{model.unhealthyDrivers.length}} of {{model.detectedDrivers.length}} {{pluralize "driver" model.detectedDrivers.length}} unhealthy
|
||||
{{else}}
|
||||
All healthy
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -75,6 +93,95 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div data-test-client-events class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Client Events
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{#list-table source=sortedEvents class="is-striped" as |t|}}
|
||||
{{#t.head}}
|
||||
<th class="is-2">Time</th>
|
||||
<th class="is-2">Subsystem</th>
|
||||
<th>Message</th>
|
||||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
<tr data-test-client-event>
|
||||
<td data-test-client-event-time>{{moment-format row.model.time "MM/DD/YY HH:mm:ss"}}</td>
|
||||
<td data-test-client-event-subsystem>{{row.model.subsystem}}</td>
|
||||
<td data-test-client-event-message>
|
||||
{{#if row.model.message}}
|
||||
{{#if row.model.driver}}
|
||||
<span class="badge is-secondary is-small">{{row.model.driver}}</span>
|
||||
{{/if}}
|
||||
{{row.model.message}}
|
||||
{{else}}
|
||||
<em>No message</em>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-test-driver-status class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Driver Status
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
{{#list-accordion source=sortedDrivers key="name" as |a|}}
|
||||
{{#a.head buttonLabel="details" isExpandable=a.item.detected}}
|
||||
<div class="columns inline-definitions {{unless a.item.detected "is-faded"}}">
|
||||
<div class="column is-1">
|
||||
<span data-test-name>{{a.item.name}}</span>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
{{#if a.item.detected}}
|
||||
<span data-test-health>
|
||||
<span class="color-swatch {{a.item.healthClass}}"></span>
|
||||
{{if a.item.healthy "Healthy" "Unhealthy"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="column">
|
||||
<span class="pair">
|
||||
<span class="term">Detected</span>
|
||||
<span data-test-detected>{{if a.item.detected "Yes" "No"}}</span>
|
||||
</span>
|
||||
<span class="is-pulled-right">
|
||||
<span class="pair">
|
||||
<span class="term">Last Updated</span>
|
||||
<span data-test-last-updated>{{moment-from-now a.item.updateTime interval=1000}}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/a.head}}
|
||||
{{#a.body}}
|
||||
<p data-test-health-description class="message">{{a.item.healthDescription}}</p>
|
||||
<div data-test-driver-attributes class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
{{capitalize a.item.name}} Attributes
|
||||
</div>
|
||||
{{#if a.item.attributes.attributesStructured}}
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{attributes-table
|
||||
attributes=a.item.attributesShort
|
||||
class="attributes-table"}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="boxed-section-body">
|
||||
<div class="empty-message">
|
||||
<h3 class="empty-message-headline">No Driver Attributes</h3>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/a.body}}
|
||||
{{/list-accordion}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Attributes
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
sortDescending=sortDescending
|
||||
class="with-foot" as |t|}}
|
||||
{{#t.head}}
|
||||
<th class="is-narrow"></th>
|
||||
{{#t.sort-by prop="id"}}ID{{/t.sort-by}}
|
||||
{{#t.sort-by class="is-200px is-truncatable" prop="name"}}Name{{/t.sort-by}}
|
||||
{{#t.sort-by prop="status"}}Status{{/t.sort-by}}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
<td data-test-indicators class="is-narrow">
|
||||
{{#if allocation.unhealthyDrivers.length}}
|
||||
<span data-test-icon="unhealthy-driver" class="tooltip text-center" aria-label="Allocation depends on unhealthy drivers">
|
||||
{{x-icon "warning" class="is-warning"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{#if allocation.nextAllocation}}
|
||||
<span class="tooltip text-center" aria-label="Allocation was rescheduled">
|
||||
<span data-test-icon="reschedule" class="tooltip text-center" aria-label="Allocation was rescheduled">
|
||||
{{x-icon "history" class="is-faded"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
<td data-test-icon class="is-narrow">
|
||||
{{#if node.unhealthyDrivers.length}}
|
||||
<span class="tooltip text-center" aria-label="Client has unhealthy drivers">
|
||||
{{x-icon "warning" class="is-warning"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-test-client-id>{{#link-to "clients.client" node.id class="is-primary"}}{{node.shortId}}{{/link-to}}</td>
|
||||
<td data-test-client-name class="is-200px is-truncatable" title="{{node.name}}">{{node.name}}</td>
|
||||
<td data-test-client-status>{{node.status}}</td>
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{{#each decoratedSource as |item|}}
|
||||
{{yield (hash
|
||||
head=(component "list-accordion/accordion-head"
|
||||
isOpen=item.isOpen
|
||||
onOpen=(action (mut item.isOpen) true)
|
||||
onClose=(action (mut item.isOpen) false))
|
||||
body=(component "list-accordion/accordion-body" isOpen=item.isOpen)
|
||||
item=item.item
|
||||
)}}
|
||||
{{/each}}
|
|
@ -0,0 +1,5 @@
|
|||
{{#if isOpen}}
|
||||
<div data-test-accordion-body class="accordion-body">
|
||||
{{yield}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -0,0 +1,9 @@
|
|||
<div class="accordion-head-content">
|
||||
{{yield}}
|
||||
</div>
|
||||
<button
|
||||
data-test-accordion-toggle
|
||||
class="button is-light is-compact pull-right accordion-toggle {{unless isExpandable "is-invisible"}}"
|
||||
onclick={{action (if isOpen onClose onOpen) item}}>
|
||||
{{buttonLabel}}
|
||||
</button>
|
|
@ -1,5 +1,5 @@
|
|||
{{#if isIdle}}
|
||||
<button data-test-idle-button type="button" class="button is-warning is-small is-inline" onclick={{action "promptForConfirmation"}}>
|
||||
<button data-test-idle-button type="button" class="button is-danger is-outlined is-important is-small is-inline" onclick={{action "promptForConfirmation"}}>
|
||||
{{idleText}}
|
||||
</button>
|
||||
{{else if isPendingConfirmation}}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { Factory, faker } from 'ember-cli-mirage';
|
||||
import { provide } from '../utils';
|
||||
|
||||
const REF_TIME = new Date();
|
||||
const STATES = provide(10, faker.system.fileExt.bind(faker.system));
|
||||
|
||||
export default Factory.extend({
|
||||
subsystem: faker.list.random(...STATES),
|
||||
message: () => faker.lorem.sentence(),
|
||||
time: () => faker.date.past(2 / 365, REF_TIME),
|
||||
details: null,
|
||||
});
|
|
@ -4,6 +4,7 @@ import { DATACENTERS, HOSTS } from '../common';
|
|||
|
||||
const UUIDS = provide(100, faker.random.uuid.bind(faker.random));
|
||||
const NODE_STATUSES = ['initializing', 'ready', 'down'];
|
||||
const REF_DATE = new Date();
|
||||
|
||||
export default Factory.extend({
|
||||
id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]),
|
||||
|
@ -28,6 +29,8 @@ export default Factory.extend({
|
|||
},
|
||||
}),
|
||||
|
||||
drivers: makeDrivers,
|
||||
|
||||
attributes() {
|
||||
// TODO add variability to these
|
||||
return {
|
||||
|
@ -72,5 +75,42 @@ export default Factory.extend({
|
|||
server.create('client-stats', {
|
||||
id: node.httpAddr,
|
||||
});
|
||||
|
||||
const events = server.createList('node-event', faker.random.number({ min: 1, max: 10 }), {
|
||||
nodeId: node.id,
|
||||
});
|
||||
|
||||
node.update({
|
||||
eventIds: events.mapBy('id'),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function makeDrivers() {
|
||||
const generate = name => {
|
||||
const detected = Math.random() > 0.3;
|
||||
const healthy = detected && Math.random() > 0.3;
|
||||
const attributes = {
|
||||
[`driver.${name}.version`]: '1.0.0',
|
||||
[`driver.${name}.status`]: 'awesome',
|
||||
[`driver.${name}.more.details`]: 'yeah',
|
||||
[`driver.${name}.more.again`]: 'we got that',
|
||||
};
|
||||
return {
|
||||
Detected: detected,
|
||||
Healthy: healthy,
|
||||
HealthDescription: healthy ? 'Driver is healthy' : 'Uh oh',
|
||||
UpdateTime: faker.date.past(5 / 365, REF_DATE),
|
||||
Attributes: Math.random() > 0.3 && detected ? attributes : null,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
docker: generate('docker'),
|
||||
rkt: generate('rkt'),
|
||||
qemu: generate('qemu'),
|
||||
exec: generate('exec'),
|
||||
raw_exec: generate('raw_exec'),
|
||||
java: generate('java'),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Factory, faker, trait } from 'ember-cli-mirage';
|
||||
import { Factory, faker } from 'ember-cli-mirage';
|
||||
import { provide } from '../utils';
|
||||
|
||||
const REF_TIME = new Date();
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { Factory, faker } from 'ember-cli-mirage';
|
||||
import { generateResources } from '../common';
|
||||
|
||||
const DRIVERS = ['docker', 'java', 'rkt', 'qemu', 'exec', 'raw_exec'];
|
||||
|
||||
export default Factory.extend({
|
||||
// Hidden property used to compute the Summary hash
|
||||
groupNames: [],
|
||||
|
@ -8,6 +10,7 @@ export default Factory.extend({
|
|||
JobID: '',
|
||||
|
||||
name: id => `task-${faker.hacker.noun()}-${id}`,
|
||||
driver: faker.list.random(...DRIVERS),
|
||||
|
||||
Resources: generateResources,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { Model, hasMany } from 'ember-cli-mirage';
|
||||
|
||||
export default Model.extend({
|
||||
events: hasMany('node-event'),
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
embed: true,
|
||||
include: ['events'],
|
||||
});
|
|
@ -72,6 +72,7 @@
|
|||
"json-formatter-js": "^2.2.0",
|
||||
"lint-staged": "^6.0.0",
|
||||
"loader.js": "^4.2.3",
|
||||
"lodash.intersection": "^4.4.0",
|
||||
"prettier": "^1.4.4",
|
||||
"query-string": "^5.0.0"
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import { assign } from '@ember/polyfills';
|
||||
import { click, findAll, currentURL, find, visit, waitFor } from 'ember-native-dom-helpers';
|
||||
import { test } from 'qunit';
|
||||
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
|
||||
|
@ -13,9 +14,24 @@ moduleForAcceptance('Acceptance | allocation detail', {
|
|||
server.create('agent');
|
||||
|
||||
node = server.create('node');
|
||||
job = server.create('job', { groupCount: 0 });
|
||||
job = server.create('job', { groupCount: 0, createAllocations: false });
|
||||
allocation = server.create('allocation', 'withTaskWithPorts');
|
||||
|
||||
// Make sure the node has an unhealthy driver
|
||||
node.update({
|
||||
driver: assign(node.drivers, {
|
||||
docker: {
|
||||
detected: true,
|
||||
healthy: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Make sure a task for the allocation depends on the unhealthy driver
|
||||
server.schema.tasks.first().update({
|
||||
driver: 'docker',
|
||||
});
|
||||
|
||||
visit(`/allocations/${allocation.id}`);
|
||||
},
|
||||
});
|
||||
|
@ -121,6 +137,10 @@ test('each task row should list high-level information for the task', function(a
|
|||
});
|
||||
});
|
||||
|
||||
test('tasks with an unhealthy driver have a warning icon', function(assert) {
|
||||
assert.ok(find('[data-test-task-row] [data-test-icon="unhealthy-driver"]'), 'Warning is shown');
|
||||
});
|
||||
|
||||
test('when the allocation has not been rescheduled, the reschedule events section is not rendered', function(assert) {
|
||||
assert.notOk(find('[data-test-reschedule-events]'), 'Reschedule Events section exists');
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { assign } from '@ember/polyfills';
|
||||
import $ from 'jquery';
|
||||
import { click, find, findAll, currentURL, visit } from 'ember-native-dom-helpers';
|
||||
import { test } from 'qunit';
|
||||
|
@ -24,12 +25,12 @@ test('/clients/:id should have a breadcrumb trail linking back to clients', func
|
|||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
find('[data-test-breadcrumb="clients"]').textContent,
|
||||
find('[data-test-breadcrumb="clients"]').textContent.trim(),
|
||||
'Clients',
|
||||
'First breadcrumb says clients'
|
||||
);
|
||||
assert.equal(
|
||||
find('[data-test-breadcrumb="client"]').textContent,
|
||||
find('[data-test-breadcrumb="client"]').textContent.trim(),
|
||||
node.id.split('-')[0],
|
||||
'Second breadcrumb says the node short id'
|
||||
);
|
||||
|
@ -58,23 +59,26 @@ test('/clients/:id should list additional detail for the node below the title',
|
|||
visit(`/clients/${node.id}`);
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
findAll('.inline-definitions .pair')[0].textContent,
|
||||
`Status ${node.status}`,
|
||||
assert.ok(
|
||||
find('.inline-definitions .pair')
|
||||
.textContent.trim()
|
||||
.includes(node.status),
|
||||
'Status is in additional details'
|
||||
);
|
||||
assert.ok(
|
||||
$('[data-test-status-definition] .status-text').hasClass(`node-${node.status}`),
|
||||
'Status is decorated with a status class'
|
||||
);
|
||||
assert.equal(
|
||||
find('[data-test-address-definition]').textContent,
|
||||
`Address ${node.httpAddr}`,
|
||||
assert.ok(
|
||||
find('[data-test-address-definition]')
|
||||
.textContent.trim()
|
||||
.includes(node.httpAddr),
|
||||
'Address is in additional details'
|
||||
);
|
||||
assert.equal(
|
||||
find('[data-test-datacenter-definition]').textContent,
|
||||
`Datacenter ${node.datacenter}`,
|
||||
assert.ok(
|
||||
find('[data-test-datacenter-definition]')
|
||||
.textContent.trim()
|
||||
.includes(node.datacenter),
|
||||
'Datacenter is in additional details'
|
||||
);
|
||||
});
|
||||
|
@ -330,9 +334,174 @@ test('when the node is not found, an error message is shown, but the URL persist
|
|||
assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists');
|
||||
assert.ok(find('[data-test-error]'), 'Error message is shown');
|
||||
assert.equal(
|
||||
find('[data-test-error-title]').textContent,
|
||||
find('[data-test-error-title]').textContent.trim(),
|
||||
'Not Found',
|
||||
'Error message is for 404'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('/clients/:id shows the recent events list', function(assert) {
|
||||
visit(`/clients/${node.id}`);
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(find('[data-test-client-events]'), 'Client events section exists');
|
||||
});
|
||||
});
|
||||
|
||||
test('each node event shows basic node event information', function(assert) {
|
||||
const event = server.db.nodeEvents
|
||||
.where({ nodeId: node.id })
|
||||
.sortBy('time')
|
||||
.reverse()[0];
|
||||
|
||||
visit(`/clients/${node.id}`);
|
||||
|
||||
andThen(() => {
|
||||
const eventRow = $(find('[data-test-client-event]'));
|
||||
assert.equal(
|
||||
eventRow
|
||||
.find('[data-test-client-event-time]')
|
||||
.text()
|
||||
.trim(),
|
||||
moment(event.time).format('MM/DD/YY HH:mm:ss'),
|
||||
'Event timestamp'
|
||||
);
|
||||
assert.equal(
|
||||
eventRow
|
||||
.find('[data-test-client-event-subsystem]')
|
||||
.text()
|
||||
.trim(),
|
||||
event.subsystem,
|
||||
'Event subsystem'
|
||||
);
|
||||
assert.equal(
|
||||
eventRow
|
||||
.find('[data-test-client-event-message]')
|
||||
.text()
|
||||
.trim(),
|
||||
event.message,
|
||||
'Event message'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('/clients/:id shows the driver status of every driver for the node', function(assert) {
|
||||
// Set the drivers up so health and detection is well tested
|
||||
const nodeDrivers = node.drivers;
|
||||
const undetectedDriver = 'raw_exec';
|
||||
|
||||
Object.values(nodeDrivers).forEach(driver => {
|
||||
driver.Detected = true;
|
||||
});
|
||||
|
||||
nodeDrivers[undetectedDriver].Detected = false;
|
||||
node.drivers = nodeDrivers;
|
||||
|
||||
const drivers = Object.keys(node.drivers)
|
||||
.map(driverName => assign({ Name: driverName }, node.drivers[driverName]))
|
||||
.sortBy('Name');
|
||||
|
||||
assert.ok(drivers.length > 0, 'Node has drivers');
|
||||
|
||||
visit(`/clients/${node.id}`);
|
||||
|
||||
andThen(() => {
|
||||
const driverRows = findAll('[data-test-driver-status] [data-test-accordion-head]');
|
||||
|
||||
drivers.forEach((driver, index) => {
|
||||
const driverRow = $(driverRows[index]);
|
||||
|
||||
assert.equal(
|
||||
driverRow
|
||||
.find('[data-test-name]')
|
||||
.text()
|
||||
.trim(),
|
||||
driver.Name,
|
||||
`${driver.Name}: Name is correct`
|
||||
);
|
||||
assert.equal(
|
||||
driverRow
|
||||
.find('[data-test-detected]')
|
||||
.text()
|
||||
.trim(),
|
||||
driver.Detected ? 'Yes' : 'No',
|
||||
`${driver.Name}: Detection is correct`
|
||||
);
|
||||
assert.equal(
|
||||
driverRow
|
||||
.find('[data-test-last-updated]')
|
||||
.text()
|
||||
.trim(),
|
||||
moment(driver.UpdateTime).fromNow(),
|
||||
`${driver.Name}: Last updated shows time since now`
|
||||
);
|
||||
|
||||
if (driver.Name === undetectedDriver) {
|
||||
assert.notOk(
|
||||
driverRow.find('[data-test-health]').length,
|
||||
`${driver.Name}: No health for the undetected driver`
|
||||
);
|
||||
} else {
|
||||
assert.equal(
|
||||
driverRow
|
||||
.find('[data-test-health]')
|
||||
.text()
|
||||
.trim(),
|
||||
driver.Healthy ? 'Healthy' : 'Unhealthy',
|
||||
`${driver.Name}: Health is correct`
|
||||
);
|
||||
assert.ok(
|
||||
driverRow
|
||||
.find('[data-test-health] .color-swatch')
|
||||
.hasClass(driver.Healthy ? 'running' : 'failed'),
|
||||
`${driver.Name}: Swatch with correct class is shown`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('each driver can be opened to see a message and attributes', function(assert) {
|
||||
// Only detected drivers can be expanded
|
||||
const nodeDrivers = node.drivers;
|
||||
Object.values(nodeDrivers).forEach(driver => {
|
||||
driver.Detected = true;
|
||||
});
|
||||
node.drivers = nodeDrivers;
|
||||
|
||||
const driver = Object.keys(node.drivers)
|
||||
.map(driverName => assign({ Name: driverName }, node.drivers[driverName]))
|
||||
.sortBy('Name')[0];
|
||||
|
||||
visit(`/clients/${node.id}`);
|
||||
|
||||
andThen(() => {
|
||||
const driverBody = $(find('[data-test-driver-status] [data-test-accordion-body]'));
|
||||
assert.notOk(
|
||||
driverBody.find('[data-test-health-description]').length,
|
||||
'Driver health description is not shown'
|
||||
);
|
||||
assert.notOk(
|
||||
driverBody.find('[data-test-driver-attributes]').length,
|
||||
'Driver attributes section is not shown'
|
||||
);
|
||||
click('[data-test-driver-status] [data-test-accordion-toggle]');
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
const driverBody = $(find('[data-test-driver-status] [data-test-accordion-body]'));
|
||||
assert.equal(
|
||||
driverBody
|
||||
.find('[data-test-health-description]')
|
||||
.text()
|
||||
.trim(),
|
||||
driver.HealthDescription,
|
||||
'Driver health description is now shown'
|
||||
);
|
||||
assert.ok(
|
||||
driverBody.find('[data-test-driver-attributes]').length,
|
||||
'Driver attributes section is now shown'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -237,8 +237,11 @@ test('when the allocation has reschedule events, the allocation row is denoted w
|
|||
const normalRow = find(`[data-test-allocation="${allocations[1].id}"]`);
|
||||
|
||||
assert.ok(
|
||||
rescheduleRow.querySelector('[data-test-indicators] .icon'),
|
||||
'Reschedule row has an icon'
|
||||
rescheduleRow.querySelector('[data-test-indicators] [data-test-icon="reschedule"]'),
|
||||
'Reschedule row has a reschedule icon'
|
||||
);
|
||||
assert.notOk(
|
||||
normalRow.querySelector('[data-test-indicators] [data-test-icon="reschedule"]'),
|
||||
'Normal row has no reschedule icon'
|
||||
);
|
||||
assert.notOk(normalRow.querySelector('[data-test-indicators] .icon'), 'Normal row has no icon');
|
||||
});
|
||||
|
|
|
@ -4,11 +4,14 @@ import wait from 'ember-test-helpers/wait';
|
|||
import hbs from 'htmlbars-inline-precompile';
|
||||
import generateResources from '../../mirage/data/generate-resources';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
import { find } from 'ember-native-dom-helpers';
|
||||
import Response from 'ember-cli-mirage/response';
|
||||
import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';
|
||||
|
||||
moduleForComponent('allocation-row', 'Integration | Component | allocation row', {
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
fragmentSerializerInitializer(getOwner(this));
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
|
@ -83,3 +86,40 @@ test('Allocation row polls for stats, even when it errors or has an invalid resp
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Allocation row shows warning when it requires drivers that are unhealthy on the node it is running on', function(assert) {
|
||||
const node = this.server.schema.nodes.first();
|
||||
const drivers = node.drivers;
|
||||
Object.values(drivers).forEach(driver => {
|
||||
driver.Healthy = false;
|
||||
driver.Detected = true;
|
||||
});
|
||||
node.update({ drivers });
|
||||
|
||||
this.server.create('allocation');
|
||||
this.store.findAll('job');
|
||||
this.store.findAll('node');
|
||||
this.store.findAll('allocation');
|
||||
|
||||
let allocation;
|
||||
|
||||
return wait()
|
||||
.then(() => {
|
||||
allocation = this.store.peekAll('allocation').get('firstObject');
|
||||
|
||||
this.setProperties({
|
||||
allocation,
|
||||
context: 'job',
|
||||
});
|
||||
|
||||
this.render(hbs`
|
||||
{{allocation-row
|
||||
allocation=allocation
|
||||
context=context}}
|
||||
`);
|
||||
return wait();
|
||||
})
|
||||
.then(() => {
|
||||
assert.ok(find('[data-test-icon="unhealthy-driver"]'), 'Unhealthy driver icon is shown');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import EmberObject from '@ember/object';
|
||||
import { run } from '@ember/runloop';
|
||||
import { assign } from '@ember/polyfills';
|
||||
import { test } from 'ember-qunit';
|
||||
|
@ -10,8 +11,12 @@ moduleForAdapter('job', 'Unit | Adapter | Job', {
|
|||
'adapter:job',
|
||||
'service:token',
|
||||
'service:system',
|
||||
'model:namespace',
|
||||
'model:allocation',
|
||||
'model:deployment',
|
||||
'model:evaluation',
|
||||
'model:job-summary',
|
||||
'model:job-version',
|
||||
'model:namespace',
|
||||
'adapter:application',
|
||||
'service:watchList',
|
||||
],
|
||||
|
@ -292,6 +297,36 @@ test('requests can be canceled even if multiple requests for the same URL were m
|
|||
});
|
||||
});
|
||||
|
||||
test('canceling a find record request will never cancel a request with the same url but different method', function(assert) {
|
||||
const { pretender } = this.server;
|
||||
const jobId = JSON.stringify(['job-1', 'default']);
|
||||
|
||||
pretender.get('/v1/job/:id', () => [200, {}, '{}'], true);
|
||||
pretender.delete('/v1/job/:id', () => [204, {}, ''], 200);
|
||||
|
||||
this.subject().findRecord(null, { modelName: 'job' }, jobId, {
|
||||
reload: true,
|
||||
adapterOptions: { watch: true },
|
||||
});
|
||||
|
||||
this.subject().stop(EmberObject.create({ id: jobId }));
|
||||
|
||||
const { request: getXHR } = pretender.requestReferences[0];
|
||||
const { request: deleteXHR } = pretender.requestReferences[1];
|
||||
assert.equal(getXHR.status, 0, 'Get request is still pending');
|
||||
assert.equal(deleteXHR.status, 0, 'Delete request is still pending');
|
||||
|
||||
// Schedule the cancelation before waiting
|
||||
run.next(() => {
|
||||
this.subject().cancelFindRecord('job', jobId);
|
||||
});
|
||||
|
||||
return wait().then(() => {
|
||||
assert.ok(getXHR.aborted, 'Get request was aborted');
|
||||
assert.notOk(deleteXHR.aborted, 'Delete request was aborted');
|
||||
});
|
||||
});
|
||||
|
||||
function makeMockModel(id, options) {
|
||||
return assign(
|
||||
{
|
||||
|
|
|
@ -9,6 +9,9 @@ moduleForAdapter('node', 'Unit | Adapter | Node', {
|
|||
'adapter:node',
|
||||
'model:node-attributes',
|
||||
'model:allocation',
|
||||
'model:node-driver',
|
||||
'model:node-event',
|
||||
'model:evaluation',
|
||||
'model:job',
|
||||
'serializer:application',
|
||||
'serializer:node',
|
||||
|
@ -16,6 +19,7 @@ moduleForAdapter('node', 'Unit | Adapter | Node', {
|
|||
'service:config',
|
||||
'service:watchList',
|
||||
'transform:fragment',
|
||||
'transform:fragment-array',
|
||||
],
|
||||
beforeEach() {
|
||||
this.server = startMirage();
|
||||
|
|
|
@ -6,7 +6,13 @@ import moduleForSerializer from '../../helpers/module-for-serializer';
|
|||
import pushPayloadToStore from '../../utils/push-payload-to-store';
|
||||
|
||||
moduleForSerializer('node', 'Unit | Serializer | Node', {
|
||||
needs: ['serializer:node', 'service:config', 'transform:fragment', 'model:allocation'],
|
||||
needs: [
|
||||
'serializer:node',
|
||||
'service:config',
|
||||
'transform:fragment',
|
||||
'transform:fragment-array',
|
||||
'model:allocation',
|
||||
],
|
||||
});
|
||||
|
||||
test('local store is culled to reflect the state of findAll requests', function(assert) {
|
||||
|
|
|
@ -5691,6 +5691,10 @@ lodash.identity@~2.3.0:
|
|||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.identity/-/lodash.identity-2.3.0.tgz#6b01a210c9485355c2a913b48b6711219a173ded"
|
||||
|
||||
lodash.intersection@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.intersection/-/lodash.intersection-4.4.0.tgz#0a11ba631d0e95c23c7f2f4cbb9a692ed178e705"
|
||||
|
||||
lodash.isarguments@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
|
||||
|
|
|
@ -58,14 +58,18 @@ the [Agent's Gossip and RPC Encryption](/docs/agent/encryption.html).
|
|||
cluster is being upgraded to TLS, and removed after the migration is
|
||||
complete. This allows the agent to accept both TLS and plaintext traffic.
|
||||
|
||||
- `tls_cipher_suites` - Specifies the TLS cipher suites that will be used by
|
||||
the agent. Known insecure ciphers are disabled (3DES and RC4). By default,
|
||||
an agent is configured to use TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
- `tls_cipher_suites` `(array<string>: [])` - Specifies the TLS cipher suites
|
||||
that will be used by the agent. Known insecure ciphers are disabled (3DES and
|
||||
RC4). By default, an agent is configured to use
|
||||
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, and
|
||||
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384.
|
||||
|
||||
- `tls_min_version` - Specifies the minimum supported version of TLS. Accepted
|
||||
values are "tls10", "tls11", "tls12". Defaults to TLS 1.2.
|
||||
- `tls_min_version` `(string: "tls12")`- Specifies the minimum supported version
|
||||
of TLS. Accepted values are "tls10", "tls11", "tls12".
|
||||
|
||||
- `tls_prefer_server_cipher_suites` `(bool: false)` - Specifies whether
|
||||
TLS connections should prefer the server's ciphersuites over the client's.
|
||||
|
||||
- `verify_https_client` `(bool: false)` - Specifies agents should require
|
||||
client certificates for all incoming HTTPS requests. The client certificates
|
||||
|
|
Loading…
Reference in New Issue