diff --git a/.ci/docker-compose-file/.env b/.ci/docker-compose-file/.env index 397c44854..956750e00 100644 --- a/.ci/docker-compose-file/.env +++ b/.ci/docker-compose-file/.env @@ -6,5 +6,6 @@ LDAP_TAG=2.4.50 INFLUXDB_TAG=2.5.0 TDENGINE_TAG=3.0.2.4 DYNAMO_TAG=1.21.0 +CASSANDRA_TAG=3.11.6 TARGET=emqx/emqx diff --git a/.ci/docker-compose-file/cassandra/Dockerfile b/.ci/docker-compose-file/cassandra/Dockerfile new file mode 100644 index 000000000..f974c1b6f --- /dev/null +++ b/.ci/docker-compose-file/cassandra/Dockerfile @@ -0,0 +1,4 @@ +ARG CASSANDRA_TAG=3.11.6 +FROM cassandra:${CASSANDRA_TAG} +COPY cassandra.yaml /etc/cassandra/cassandra.yaml +CMD ["cassandra", "-f"] diff --git a/.ci/docker-compose-file/cassandra/cassandra.yaml b/.ci/docker-compose-file/cassandra/cassandra.yaml new file mode 100644 index 000000000..968efe5f6 --- /dev/null +++ b/.ci/docker-compose-file/cassandra/cassandra.yaml @@ -0,0 +1,1236 @@ +# Cassandra storage config YAML + +# NOTE: +# See http://wiki.apache.org/cassandra/StorageConfiguration for +# full explanations of configuration directives +# /NOTE + +# The name of the cluster. This is mainly used to prevent machines in +# one logical cluster from joining another. +cluster_name: 'Test Cluster' + +# This defines the number of tokens randomly assigned to this node on the ring +# The more tokens, relative to other nodes, the larger the proportion of data +# that this node will store. You probably want all nodes to have the same number +# of tokens assuming they have equal hardware capability. +# +# If you leave this unspecified, Cassandra will use the default of 1 token for legacy compatibility, +# and will use the initial_token as described below. +# +# Specifying initial_token will override this setting on the node's initial start, +# on subsequent starts, this setting will apply even if initial token is set. +# +# If you already have a cluster with 1 token per node, and wish to migrate to +# multiple tokens per node, see http://wiki.apache.org/cassandra/Operations +num_tokens: 256 + +# Triggers automatic allocation of num_tokens tokens for this node. The allocation +# algorithm attempts to choose tokens in a way that optimizes replicated load over +# the nodes in the datacenter for the replication strategy used by the specified +# keyspace. +# +# The load assigned to each node will be close to proportional to its number of +# vnodes. +# +# Only supported with the Murmur3Partitioner. +# allocate_tokens_for_keyspace: KEYSPACE + +# initial_token allows you to specify tokens manually. While you can use it with +# vnodes (num_tokens > 1, above) -- in which case you should provide a +# comma-separated list -- it's primarily used when adding nodes to legacy clusters +# that do not have vnodes enabled. +# initial_token: + +# See http://wiki.apache.org/cassandra/HintedHandoff +# May either be "true" or "false" to enable globally +hinted_handoff_enabled: true + +# When hinted_handoff_enabled is true, a black list of data centers that will not +# perform hinted handoff +# hinted_handoff_disabled_datacenters: +# - DC1 +# - DC2 + +# this defines the maximum amount of time a dead host will have hints +# generated. After it has been dead this long, new hints for it will not be +# created until it has been seen alive and gone down again. +max_hint_window_in_ms: 10800000 # 3 hours + +# Maximum throttle in KBs per second, per delivery thread. This will be +# reduced proportionally to the number of nodes in the cluster. (If there +# are two nodes in the cluster, each delivery thread will use the maximum +# rate; if there are three, each will throttle to half of the maximum, +# since we expect two nodes to be delivering hints simultaneously.) +hinted_handoff_throttle_in_kb: 1024 + +# Number of threads with which to deliver hints; +# Consider increasing this number when you have multi-dc deployments, since +# cross-dc handoff tends to be slower +max_hints_delivery_threads: 2 + +# Directory where Cassandra should store hints. +# If not set, the default directory is $CASSANDRA_HOME/data/hints. +# hints_directory: /var/lib/cassandra/hints + +# How often hints should be flushed from the internal buffers to disk. +# Will *not* trigger fsync. +hints_flush_period_in_ms: 10000 + +# Maximum size for a single hints file, in megabytes. +max_hints_file_size_in_mb: 128 + +# Compression to apply to the hint files. If omitted, hints files +# will be written uncompressed. LZ4, Snappy, and Deflate compressors +# are supported. +#hints_compression: +# - class_name: LZ4Compressor +# parameters: +# - + +# Maximum throttle in KBs per second, total. This will be +# reduced proportionally to the number of nodes in the cluster. +batchlog_replay_throttle_in_kb: 1024 + +# Authentication backend, implementing IAuthenticator; used to identify users +# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator, +# PasswordAuthenticator}. +# +# - AllowAllAuthenticator performs no checks - set it to disable authentication. +# - PasswordAuthenticator relies on username/password pairs to authenticate +# users. It keeps usernames and hashed passwords in system_auth.roles table. +# Please increase system_auth keyspace replication factor if you use this authenticator. +# If using PasswordAuthenticator, CassandraRoleManager must also be used (see below) +authenticator: PasswordAuthenticator + +# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions +# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer, +# CassandraAuthorizer}. +# +# - AllowAllAuthorizer allows any action to any user - set it to disable authorization. +# - CassandraAuthorizer stores permissions in system_auth.role_permissions table. Please +# increase system_auth keyspace replication factor if you use this authorizer. +authorizer: AllowAllAuthorizer + +# Part of the Authentication & Authorization backend, implementing IRoleManager; used +# to maintain grants and memberships between roles. +# Out of the box, Cassandra provides org.apache.cassandra.auth.CassandraRoleManager, +# which stores role information in the system_auth keyspace. Most functions of the +# IRoleManager require an authenticated login, so unless the configured IAuthenticator +# actually implements authentication, most of this functionality will be unavailable. +# +# - CassandraRoleManager stores role data in the system_auth keyspace. Please +# increase system_auth keyspace replication factor if you use this role manager. +role_manager: CassandraRoleManager + +# Validity period for roles cache (fetching granted roles can be an expensive +# operation depending on the role manager, CassandraRoleManager is one example) +# Granted roles are cached for authenticated sessions in AuthenticatedUser and +# after the period specified here, become eligible for (async) reload. +# Defaults to 2000, set to 0 to disable caching entirely. +# Will be disabled automatically for AllowAllAuthenticator. +roles_validity_in_ms: 2000 + +# Refresh interval for roles cache (if enabled). +# After this interval, cache entries become eligible for refresh. Upon next +# access, an async reload is scheduled and the old value returned until it +# completes. If roles_validity_in_ms is non-zero, then this must be +# also. +# Defaults to the same value as roles_validity_in_ms. +# roles_update_interval_in_ms: 2000 + +# Validity period for permissions cache (fetching permissions can be an +# expensive operation depending on the authorizer, CassandraAuthorizer is +# one example). Defaults to 2000, set to 0 to disable. +# Will be disabled automatically for AllowAllAuthorizer. +permissions_validity_in_ms: 2000 + +# Refresh interval for permissions cache (if enabled). +# After this interval, cache entries become eligible for refresh. Upon next +# access, an async reload is scheduled and the old value returned until it +# completes. If permissions_validity_in_ms is non-zero, then this must be +# also. +# Defaults to the same value as permissions_validity_in_ms. +# permissions_update_interval_in_ms: 2000 + +# Validity period for credentials cache. This cache is tightly coupled to +# the provided PasswordAuthenticator implementation of IAuthenticator. If +# another IAuthenticator implementation is configured, this cache will not +# be automatically used and so the following settings will have no effect. +# Please note, credentials are cached in their encrypted form, so while +# activating this cache may reduce the number of queries made to the +# underlying table, it may not bring a significant reduction in the +# latency of individual authentication attempts. +# Defaults to 2000, set to 0 to disable credentials caching. +credentials_validity_in_ms: 2000 + +# Refresh interval for credentials cache (if enabled). +# After this interval, cache entries become eligible for refresh. Upon next +# access, an async reload is scheduled and the old value returned until it +# completes. If credentials_validity_in_ms is non-zero, then this must be +# also. +# Defaults to the same value as credentials_validity_in_ms. +# credentials_update_interval_in_ms: 2000 + +# The partitioner is responsible for distributing groups of rows (by +# partition key) across nodes in the cluster. You should leave this +# alone for new clusters. The partitioner can NOT be changed without +# reloading all data, so when upgrading you should set this to the +# same partitioner you were already using. +# +# Besides Murmur3Partitioner, partitioners included for backwards +# compatibility include RandomPartitioner, ByteOrderedPartitioner, and +# OrderPreservingPartitioner. +# +partitioner: org.apache.cassandra.dht.Murmur3Partitioner + +# Directories where Cassandra should store data on disk. Cassandra +# will spread data evenly across them, subject to the granularity of +# the configured compaction strategy. +# If not set, the default directory is $CASSANDRA_HOME/data/data. +data_file_directories: + - /var/lib/cassandra/data + +# commit log. when running on magnetic HDD, this should be a +# separate spindle than the data directories. +# If not set, the default directory is $CASSANDRA_HOME/data/commitlog. +commitlog_directory: /var/lib/cassandra/commitlog + +# Enable / disable CDC functionality on a per-node basis. This modifies the logic used +# for write path allocation rejection (standard: never reject. cdc: reject Mutation +# containing a CDC-enabled table if at space limit in cdc_raw_directory). +cdc_enabled: false + +# CommitLogSegments are moved to this directory on flush if cdc_enabled: true and the +# segment contains mutations for a CDC-enabled table. This should be placed on a +# separate spindle than the data directories. If not set, the default directory is +# $CASSANDRA_HOME/data/cdc_raw. +# cdc_raw_directory: /var/lib/cassandra/cdc_raw + +# Policy for data disk failures: +# +# die +# shut down gossip and client transports and kill the JVM for any fs errors or +# single-sstable errors, so the node can be replaced. +# +# stop_paranoid +# shut down gossip and client transports even for single-sstable errors, +# kill the JVM for errors during startup. +# +# stop +# shut down gossip and client transports, leaving the node effectively dead, but +# can still be inspected via JMX, kill the JVM for errors during startup. +# +# best_effort +# stop using the failed disk and respond to requests based on +# remaining available sstables. This means you WILL see obsolete +# data at CL.ONE! +# +# ignore +# ignore fatal errors and let requests fail, as in pre-1.2 Cassandra +disk_failure_policy: stop + +# Policy for commit disk failures: +# +# die +# shut down gossip and Thrift and kill the JVM, so the node can be replaced. +# +# stop +# shut down gossip and Thrift, leaving the node effectively dead, but +# can still be inspected via JMX. +# +# stop_commit +# shutdown the commit log, letting writes collect but +# continuing to service reads, as in pre-2.0.5 Cassandra +# +# ignore +# ignore fatal errors and let the batches fail +commit_failure_policy: stop + +# Maximum size of the native protocol prepared statement cache +# +# Valid values are either "auto" (omitting the value) or a value greater 0. +# +# Note that specifying a too large value will result in long running GCs and possbily +# out-of-memory errors. Keep the value at a small fraction of the heap. +# +# If you constantly see "prepared statements discarded in the last minute because +# cache limit reached" messages, the first step is to investigate the root cause +# of these messages and check whether prepared statements are used correctly - +# i.e. use bind markers for variable parts. +# +# Do only change the default value, if you really have more prepared statements than +# fit in the cache. In most cases it is not neccessary to change this value. +# Constantly re-preparing statements is a performance penalty. +# +# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater +prepared_statements_cache_size_mb: + +# Maximum size of the Thrift prepared statement cache +# +# If you do not use Thrift at all, it is safe to leave this value at "auto". +# +# See description of 'prepared_statements_cache_size_mb' above for more information. +# +# Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater +thrift_prepared_statements_cache_size_mb: + +# Maximum size of the key cache in memory. +# +# Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the +# minimum, sometimes more. The key cache is fairly tiny for the amount of +# time it saves, so it's worthwhile to use it at large numbers. +# The row cache saves even more time, but must contain the entire row, +# so it is extremely space-intensive. It's best to only use the +# row cache if you have hot rows or static rows. +# +# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. +# +# Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache. +key_cache_size_in_mb: + +# Duration in seconds after which Cassandra should +# save the key cache. Caches are saved to saved_caches_directory as +# specified in this configuration file. +# +# Saved caches greatly improve cold-start speeds, and is relatively cheap in +# terms of I/O for the key cache. Row cache saving is much more expensive and +# has limited use. +# +# Default is 14400 or 4 hours. +key_cache_save_period: 14400 + +# Number of keys from the key cache to save +# Disabled by default, meaning all keys are going to be saved +# key_cache_keys_to_save: 100 + +# Row cache implementation class name. Available implementations: +# +# org.apache.cassandra.cache.OHCProvider +# Fully off-heap row cache implementation (default). +# +# org.apache.cassandra.cache.SerializingCacheProvider +# This is the row cache implementation availabile +# in previous releases of Cassandra. +# row_cache_class_name: org.apache.cassandra.cache.OHCProvider + +# Maximum size of the row cache in memory. +# Please note that OHC cache implementation requires some additional off-heap memory to manage +# the map structures and some in-flight memory during operations before/after cache entries can be +# accounted against the cache capacity. This overhead is usually small compared to the whole capacity. +# Do not specify more memory that the system can afford in the worst usual situation and leave some +# headroom for OS block level cache. Do never allow your system to swap. +# +# Default value is 0, to disable row caching. +row_cache_size_in_mb: 0 + +# Duration in seconds after which Cassandra should save the row cache. +# Caches are saved to saved_caches_directory as specified in this configuration file. +# +# Saved caches greatly improve cold-start speeds, and is relatively cheap in +# terms of I/O for the key cache. Row cache saving is much more expensive and +# has limited use. +# +# Default is 0 to disable saving the row cache. +row_cache_save_period: 0 + +# Number of keys from the row cache to save. +# Specify 0 (which is the default), meaning all keys are going to be saved +# row_cache_keys_to_save: 100 + +# Maximum size of the counter cache in memory. +# +# Counter cache helps to reduce counter locks' contention for hot counter cells. +# In case of RF = 1 a counter cache hit will cause Cassandra to skip the read before +# write entirely. With RF > 1 a counter cache hit will still help to reduce the duration +# of the lock hold, helping with hot counter cell updates, but will not allow skipping +# the read entirely. Only the local (clock, count) tuple of a counter cell is kept +# in memory, not the whole counter, so it's relatively cheap. +# +# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. +# +# Default value is empty to make it "auto" (min(2.5% of Heap (in MB), 50MB)). Set to 0 to disable counter cache. +# NOTE: if you perform counter deletes and rely on low gcgs, you should disable the counter cache. +counter_cache_size_in_mb: + +# Duration in seconds after which Cassandra should +# save the counter cache (keys only). Caches are saved to saved_caches_directory as +# specified in this configuration file. +# +# Default is 7200 or 2 hours. +counter_cache_save_period: 7200 + +# Number of keys from the counter cache to save +# Disabled by default, meaning all keys are going to be saved +# counter_cache_keys_to_save: 100 + +# saved caches +# If not set, the default directory is $CASSANDRA_HOME/data/saved_caches. +saved_caches_directory: /var/lib/cassandra/saved_caches + +# commitlog_sync may be either "periodic" or "batch." +# +# When in batch mode, Cassandra won't ack writes until the commit log +# has been fsynced to disk. It will wait +# commitlog_sync_batch_window_in_ms milliseconds between fsyncs. +# This window should be kept short because the writer threads will +# be unable to do extra work while waiting. (You may need to increase +# concurrent_writes for the same reason.) +# +# commitlog_sync: batch +# commitlog_sync_batch_window_in_ms: 2 +# +# the other option is "periodic" where writes may be acked immediately +# and the CommitLog is simply synced every commitlog_sync_period_in_ms +# milliseconds. +commitlog_sync: periodic +commitlog_sync_period_in_ms: 10000 + +# The size of the individual commitlog file segments. A commitlog +# segment may be archived, deleted, or recycled once all the data +# in it (potentially from each columnfamily in the system) has been +# flushed to sstables. +# +# The default size is 32, which is almost always fine, but if you are +# archiving commitlog segments (see commitlog_archiving.properties), +# then you probably want a finer granularity of archiving; 8 or 16 MB +# is reasonable. +# Max mutation size is also configurable via max_mutation_size_in_kb setting in +# cassandra.yaml. The default is half the size commitlog_segment_size_in_mb * 1024. +# This should be positive and less than 2048. +# +# NOTE: If max_mutation_size_in_kb is set explicitly then commitlog_segment_size_in_mb must +# be set to at least twice the size of max_mutation_size_in_kb / 1024 +# +commitlog_segment_size_in_mb: 32 + +# Compression to apply to the commit log. If omitted, the commit log +# will be written uncompressed. LZ4, Snappy, and Deflate compressors +# are supported. +# commitlog_compression: +# - class_name: LZ4Compressor +# parameters: +# - + +# any class that implements the SeedProvider interface and has a +# constructor that takes a Map of parameters will do. +seed_provider: + # Addresses of hosts that are deemed contact points. + # Cassandra nodes use this list of hosts to find each other and learn + # the topology of the ring. You must change this if you are running + # multiple nodes! + - class_name: org.apache.cassandra.locator.SimpleSeedProvider + parameters: + # seeds is actually a comma-delimited list of addresses. + # Ex: ",," + - seeds: "127.0.0.1" + +# For workloads with more data than can fit in memory, Cassandra's +# bottleneck will be reads that need to fetch data from +# disk. "concurrent_reads" should be set to (16 * number_of_drives) in +# order to allow the operations to enqueue low enough in the stack +# that the OS and drives can reorder them. Same applies to +# "concurrent_counter_writes", since counter writes read the current +# values before incrementing and writing them back. +# +# On the other hand, since writes are almost never IO bound, the ideal +# number of "concurrent_writes" is dependent on the number of cores in +# your system; (8 * number_of_cores) is a good rule of thumb. +concurrent_reads: 32 +concurrent_writes: 32 +concurrent_counter_writes: 32 + +# For materialized view writes, as there is a read involved, so this should +# be limited by the less of concurrent reads or concurrent writes. +concurrent_materialized_view_writes: 32 + +# Maximum memory to use for sstable chunk cache and buffer pooling. +# 32MB of this are reserved for pooling buffers, the rest is used as an +# cache that holds uncompressed sstable chunks. +# Defaults to the smaller of 1/4 of heap or 512MB. This pool is allocated off-heap, +# so is in addition to the memory allocated for heap. The cache also has on-heap +# overhead which is roughly 128 bytes per chunk (i.e. 0.2% of the reserved size +# if the default 64k chunk size is used). +# Memory is only allocated when needed. +# file_cache_size_in_mb: 512 + +# Flag indicating whether to allocate on or off heap when the sstable buffer +# pool is exhausted, that is when it has exceeded the maximum memory +# file_cache_size_in_mb, beyond which it will not cache buffers but allocate on request. + +# buffer_pool_use_heap_if_exhausted: true + +# The strategy for optimizing disk read +# Possible values are: +# ssd (for solid state disks, the default) +# spinning (for spinning disks) +# disk_optimization_strategy: ssd + +# Total permitted memory to use for memtables. Cassandra will stop +# accepting writes when the limit is exceeded until a flush completes, +# and will trigger a flush based on memtable_cleanup_threshold +# If omitted, Cassandra will set both to 1/4 the size of the heap. +# memtable_heap_space_in_mb: 2048 +# memtable_offheap_space_in_mb: 2048 + +# memtable_cleanup_threshold is deprecated. The default calculation +# is the only reasonable choice. See the comments on memtable_flush_writers +# for more information. +# +# Ratio of occupied non-flushing memtable size to total permitted size +# that will trigger a flush of the largest memtable. Larger mct will +# mean larger flushes and hence less compaction, but also less concurrent +# flush activity which can make it difficult to keep your disks fed +# under heavy write load. +# +# memtable_cleanup_threshold defaults to 1 / (memtable_flush_writers + 1) +# memtable_cleanup_threshold: 0.11 + +# Specify the way Cassandra allocates and manages memtable memory. +# Options are: +# +# heap_buffers +# on heap nio buffers +# +# offheap_buffers +# off heap (direct) nio buffers +# +# offheap_objects +# off heap objects +memtable_allocation_type: heap_buffers + +# Total space to use for commit logs on disk. +# +# If space gets above this value, Cassandra will flush every dirty CF +# in the oldest segment and remove it. So a small total commitlog space +# will tend to cause more flush activity on less-active columnfamilies. +# +# The default value is the smaller of 8192, and 1/4 of the total space +# of the commitlog volume. +# +# commitlog_total_space_in_mb: 8192 + +# This sets the number of memtable flush writer threads per disk +# as well as the total number of memtables that can be flushed concurrently. +# These are generally a combination of compute and IO bound. +# +# Memtable flushing is more CPU efficient than memtable ingest and a single thread +# can keep up with the ingest rate of a whole server on a single fast disk +# until it temporarily becomes IO bound under contention typically with compaction. +# At that point you need multiple flush threads. At some point in the future +# it may become CPU bound all the time. +# +# You can tell if flushing is falling behind using the MemtablePool.BlockedOnAllocation +# metric which should be 0, but will be non-zero if threads are blocked waiting on flushing +# to free memory. +# +# memtable_flush_writers defaults to two for a single data directory. +# This means that two memtables can be flushed concurrently to the single data directory. +# If you have multiple data directories the default is one memtable flushing at a time +# but the flush will use a thread per data directory so you will get two or more writers. +# +# Two is generally enough to flush on a fast disk [array] mounted as a single data directory. +# Adding more flush writers will result in smaller more frequent flushes that introduce more +# compaction overhead. +# +# There is a direct tradeoff between number of memtables that can be flushed concurrently +# and flush size and frequency. More is not better you just need enough flush writers +# to never stall waiting for flushing to free memory. +# +#memtable_flush_writers: 2 + +# Total space to use for change-data-capture logs on disk. +# +# If space gets above this value, Cassandra will throw WriteTimeoutException +# on Mutations including tables with CDC enabled. A CDCCompactor is responsible +# for parsing the raw CDC logs and deleting them when parsing is completed. +# +# The default value is the min of 4096 mb and 1/8th of the total space +# of the drive where cdc_raw_directory resides. +# cdc_total_space_in_mb: 4096 + +# When we hit our cdc_raw limit and the CDCCompactor is either running behind +# or experiencing backpressure, we check at the following interval to see if any +# new space for cdc-tracked tables has been made available. Default to 250ms +# cdc_free_space_check_interval_ms: 250 + +# A fixed memory pool size in MB for for SSTable index summaries. If left +# empty, this will default to 5% of the heap size. If the memory usage of +# all index summaries exceeds this limit, SSTables with low read rates will +# shrink their index summaries in order to meet this limit. However, this +# is a best-effort process. In extreme conditions Cassandra may need to use +# more than this amount of memory. +index_summary_capacity_in_mb: + +# How frequently index summaries should be resampled. This is done +# periodically to redistribute memory from the fixed-size pool to sstables +# proportional their recent read rates. Setting to -1 will disable this +# process, leaving existing index summaries at their current sampling level. +index_summary_resize_interval_in_minutes: 60 + +# Whether to, when doing sequential writing, fsync() at intervals in +# order to force the operating system to flush the dirty +# buffers. Enable this to avoid sudden dirty buffer flushing from +# impacting read latencies. Almost always a good idea on SSDs; not +# necessarily on platters. +trickle_fsync: false +trickle_fsync_interval_in_kb: 10240 + +# TCP port, for commands and data +# For security reasons, you should not expose this port to the internet. Firewall it if needed. +storage_port: 7000 + +# SSL port, for encrypted communication. Unused unless enabled in +# encryption_options +# For security reasons, you should not expose this port to the internet. Firewall it if needed. +ssl_storage_port: 7001 + +# Address or interface to bind to and tell other Cassandra nodes to connect to. +# You _must_ change this if you want multiple nodes to be able to communicate! +# +# Set listen_address OR listen_interface, not both. +# +# Leaving it blank leaves it up to InetAddress.getLocalHost(). This +# will always do the Right Thing _if_ the node is properly configured +# (hostname, name resolution, etc), and the Right Thing is to use the +# address associated with the hostname (it might not be). +# +# Setting listen_address to 0.0.0.0 is always wrong. +# +listen_address: localhost + +# Set listen_address OR listen_interface, not both. Interfaces must correspond +# to a single address, IP aliasing is not supported. +# listen_interface: eth0 + +# If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address +# you can specify which should be chosen using listen_interface_prefer_ipv6. If false the first ipv4 +# address will be used. If true the first ipv6 address will be used. Defaults to false preferring +# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. +# listen_interface_prefer_ipv6: false + +# Address to broadcast to other Cassandra nodes +# Leaving this blank will set it to the same value as listen_address +# broadcast_address: 1.2.3.4 + +# When using multiple physical network interfaces, set this +# to true to listen on broadcast_address in addition to +# the listen_address, allowing nodes to communicate in both +# interfaces. +# Ignore this property if the network configuration automatically +# routes between the public and private networks such as EC2. +# listen_on_broadcast_address: false + +# Internode authentication backend, implementing IInternodeAuthenticator; +# used to allow/disallow connections from peer nodes. +# internode_authenticator: org.apache.cassandra.auth.AllowAllInternodeAuthenticator + +# Whether to start the native transport server. +# Please note that the address on which the native transport is bound is the +# same as the rpc_address. The port however is different and specified below. +start_native_transport: true +# port for the CQL native transport to listen for clients on +# For security reasons, you should not expose this port to the internet. Firewall it if needed. +native_transport_port: 9042 +# Enabling native transport encryption in client_encryption_options allows you to either use +# encryption for the standard port or to use a dedicated, additional port along with the unencrypted +# standard native_transport_port. +# Enabling client encryption and keeping native_transport_port_ssl disabled will use encryption +# for native_transport_port. Setting native_transport_port_ssl to a different value +# from native_transport_port will use encryption for native_transport_port_ssl while +# keeping native_transport_port unencrypted. +native_transport_port_ssl: 9142 +# The maximum threads for handling requests when the native transport is used. +# This is similar to rpc_max_threads though the default differs slightly (and +# there is no native_transport_min_threads, idle threads will always be stopped +# after 30 seconds). +# native_transport_max_threads: 128 +# +# The maximum size of allowed frame. Frame (requests) larger than this will +# be rejected as invalid. The default is 256MB. If you're changing this parameter, +# you may want to adjust max_value_size_in_mb accordingly. This should be positive and less than 2048. +# native_transport_max_frame_size_in_mb: 256 + +# The maximum number of concurrent client connections. +# The default is -1, which means unlimited. +# native_transport_max_concurrent_connections: -1 + +# The maximum number of concurrent client connections per source ip. +# The default is -1, which means unlimited. +# native_transport_max_concurrent_connections_per_ip: -1 + +# Whether to start the thrift rpc server. +start_rpc: true + +# The address or interface to bind the Thrift RPC service and native transport +# server to. +# +# Set rpc_address OR rpc_interface, not both. +# +# Leaving rpc_address blank has the same effect as on listen_address +# (i.e. it will be based on the configured hostname of the node). +# +# Note that unlike listen_address, you can specify 0.0.0.0, but you must also +# set broadcast_rpc_address to a value other than 0.0.0.0. +# +# For security reasons, you should not expose this port to the internet. Firewall it if needed. +rpc_address: 0.0.0.0 + +# Set rpc_address OR rpc_interface, not both. Interfaces must correspond +# to a single address, IP aliasing is not supported. +# rpc_interface: eth1 + +# If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address +# you can specify which should be chosen using rpc_interface_prefer_ipv6. If false the first ipv4 +# address will be used. If true the first ipv6 address will be used. Defaults to false preferring +# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. +# rpc_interface_prefer_ipv6: false + +# port for Thrift to listen for clients on +rpc_port: 9160 + +# RPC address to broadcast to drivers and other Cassandra nodes. This cannot +# be set to 0.0.0.0. If left blank, this will be set to the value of +# rpc_address. If rpc_address is set to 0.0.0.0, broadcast_rpc_address must +# be set. +broadcast_rpc_address: 1.2.3.4 + +# enable or disable keepalive on rpc/native connections +rpc_keepalive: true + +# Cassandra provides two out-of-the-box options for the RPC Server: +# +# sync +# One thread per thrift connection. For a very large number of clients, memory +# will be your limiting factor. On a 64 bit JVM, 180KB is the minimum stack size +# per thread, and that will correspond to your use of virtual memory (but physical memory +# may be limited depending on use of stack space). +# +# hsha +# Stands for "half synchronous, half asynchronous." All thrift clients are handled +# asynchronously using a small number of threads that does not vary with the amount +# of thrift clients (and thus scales well to many clients). The rpc requests are still +# synchronous (one thread per active request). If hsha is selected then it is essential +# that rpc_max_threads is changed from the default value of unlimited. +# +# The default is sync because on Windows hsha is about 30% slower. On Linux, +# sync/hsha performance is about the same, with hsha of course using less memory. +# +# Alternatively, can provide your own RPC server by providing the fully-qualified class name +# of an o.a.c.t.TServerFactory that can create an instance of it. +rpc_server_type: sync + +# Uncomment rpc_min|max_thread to set request pool size limits. +# +# Regardless of your choice of RPC server (see above), the number of maximum requests in the +# RPC thread pool dictates how many concurrent requests are possible (but if you are using the sync +# RPC server, it also dictates the number of clients that can be connected at all). +# +# The default is unlimited and thus provides no protection against clients overwhelming the server. You are +# encouraged to set a maximum that makes sense for you in production, but do keep in mind that +# rpc_max_threads represents the maximum number of client requests this server may execute concurrently. +# +# rpc_min_threads: 16 +# rpc_max_threads: 2048 + +# uncomment to set socket buffer sizes on rpc connections +# rpc_send_buff_size_in_bytes: +# rpc_recv_buff_size_in_bytes: + +# Uncomment to set socket buffer size for internode communication +# Note that when setting this, the buffer size is limited by net.core.wmem_max +# and when not setting it it is defined by net.ipv4.tcp_wmem +# See also: +# /proc/sys/net/core/wmem_max +# /proc/sys/net/core/rmem_max +# /proc/sys/net/ipv4/tcp_wmem +# /proc/sys/net/ipv4/tcp_wmem +# and 'man tcp' +# internode_send_buff_size_in_bytes: + +# Uncomment to set socket buffer size for internode communication +# Note that when setting this, the buffer size is limited by net.core.wmem_max +# and when not setting it it is defined by net.ipv4.tcp_wmem +# internode_recv_buff_size_in_bytes: + +# Frame size for thrift (maximum message length). +thrift_framed_transport_size_in_mb: 15 + +# Set to true to have Cassandra create a hard link to each sstable +# flushed or streamed locally in a backups/ subdirectory of the +# keyspace data. Removing these links is the operator's +# responsibility. +incremental_backups: false + +# Whether or not to take a snapshot before each compaction. Be +# careful using this option, since Cassandra won't clean up the +# snapshots for you. Mostly useful if you're paranoid when there +# is a data format change. +snapshot_before_compaction: false + +# Whether or not a snapshot is taken of the data before keyspace truncation +# or dropping of column families. The STRONGLY advised default of true +# should be used to provide data safety. If you set this flag to false, you will +# lose data on truncation or drop. +auto_snapshot: true + +# Granularity of the collation index of rows within a partition. +# Increase if your rows are large, or if you have a very large +# number of rows per partition. The competing goals are these: +# +# - a smaller granularity means more index entries are generated +# and looking up rows withing the partition by collation column +# is faster +# - but, Cassandra will keep the collation index in memory for hot +# rows (as part of the key cache), so a larger granularity means +# you can cache more hot rows +column_index_size_in_kb: 64 + +# Per sstable indexed key cache entries (the collation index in memory +# mentioned above) exceeding this size will not be held on heap. +# This means that only partition information is held on heap and the +# index entries are read from disk. +# +# Note that this size refers to the size of the +# serialized index information and not the size of the partition. +column_index_cache_size_in_kb: 2 + +# Number of simultaneous compactions to allow, NOT including +# validation "compactions" for anti-entropy repair. Simultaneous +# compactions can help preserve read performance in a mixed read/write +# workload, by mitigating the tendency of small sstables to accumulate +# during a single long running compactions. The default is usually +# fine and if you experience problems with compaction running too +# slowly or too fast, you should look at +# compaction_throughput_mb_per_sec first. +# +# concurrent_compactors defaults to the smaller of (number of disks, +# number of cores), with a minimum of 2 and a maximum of 8. +# +# If your data directories are backed by SSD, you should increase this +# to the number of cores. +#concurrent_compactors: 1 + +# Throttles compaction to the given total throughput across the entire +# system. The faster you insert data, the faster you need to compact in +# order to keep the sstable count down, but in general, setting this to +# 16 to 32 times the rate you are inserting data is more than sufficient. +# Setting this to 0 disables throttling. Note that this account for all types +# of compaction, including validation compaction. +compaction_throughput_mb_per_sec: 16 + +# When compacting, the replacement sstable(s) can be opened before they +# are completely written, and used in place of the prior sstables for +# any range that has been written. This helps to smoothly transfer reads +# between the sstables, reducing page cache churn and keeping hot rows hot +sstable_preemptive_open_interval_in_mb: 50 + +# Throttles all outbound streaming file transfers on this node to the +# given total throughput in Mbps. This is necessary because Cassandra does +# mostly sequential IO when streaming data during bootstrap or repair, which +# can lead to saturating the network connection and degrading rpc performance. +# When unset, the default is 200 Mbps or 25 MB/s. +# stream_throughput_outbound_megabits_per_sec: 200 + +# Throttles all streaming file transfer between the datacenters, +# this setting allows users to throttle inter dc stream throughput in addition +# to throttling all network stream traffic as configured with +# stream_throughput_outbound_megabits_per_sec +# When unset, the default is 200 Mbps or 25 MB/s +# inter_dc_stream_throughput_outbound_megabits_per_sec: 200 + +# How long the coordinator should wait for read operations to complete +read_request_timeout_in_ms: 5000 +# How long the coordinator should wait for seq or index scans to complete +range_request_timeout_in_ms: 10000 +# How long the coordinator should wait for writes to complete +write_request_timeout_in_ms: 2000 +# How long the coordinator should wait for counter writes to complete +counter_write_request_timeout_in_ms: 5000 +# How long a coordinator should continue to retry a CAS operation +# that contends with other proposals for the same row +cas_contention_timeout_in_ms: 1000 +# How long the coordinator should wait for truncates to complete +# (This can be much longer, because unless auto_snapshot is disabled +# we need to flush first so we can snapshot before removing the data.) +truncate_request_timeout_in_ms: 60000 +# The default timeout for other, miscellaneous operations +request_timeout_in_ms: 10000 + +# How long before a node logs slow queries. Select queries that take longer than +# this timeout to execute, will generate an aggregated log message, so that slow queries +# can be identified. Set this value to zero to disable slow query logging. +slow_query_log_timeout_in_ms: 500 + +# Enable operation timeout information exchange between nodes to accurately +# measure request timeouts. If disabled, replicas will assume that requests +# were forwarded to them instantly by the coordinator, which means that +# under overload conditions we will waste that much extra time processing +# already-timed-out requests. +# +# Warning: before enabling this property make sure to ntp is installed +# and the times are synchronized between the nodes. +cross_node_timeout: false + +# Set keep-alive period for streaming +# This node will send a keep-alive message periodically with this period. +# If the node does not receive a keep-alive message from the peer for +# 2 keep-alive cycles the stream session times out and fail +# Default value is 300s (5 minutes), which means stalled stream +# times out in 10 minutes by default +# streaming_keep_alive_period_in_secs: 300 + +# phi value that must be reached for a host to be marked down. +# most users should never need to adjust this. +# phi_convict_threshold: 8 + +# endpoint_snitch -- Set this to a class that implements +# IEndpointSnitch. The snitch has two functions: +# +# - it teaches Cassandra enough about your network topology to route +# requests efficiently +# - it allows Cassandra to spread replicas around your cluster to avoid +# correlated failures. It does this by grouping machines into +# "datacenters" and "racks." Cassandra will do its best not to have +# more than one replica on the same "rack" (which may not actually +# be a physical location) +# +# CASSANDRA WILL NOT ALLOW YOU TO SWITCH TO AN INCOMPATIBLE SNITCH +# ONCE DATA IS INSERTED INTO THE CLUSTER. This would cause data loss. +# This means that if you start with the default SimpleSnitch, which +# locates every node on "rack1" in "datacenter1", your only options +# if you need to add another datacenter are GossipingPropertyFileSnitch +# (and the older PFS). From there, if you want to migrate to an +# incompatible snitch like Ec2Snitch you can do it by adding new nodes +# under Ec2Snitch (which will locate them in a new "datacenter") and +# decommissioning the old ones. +# +# Out of the box, Cassandra provides: +# +# SimpleSnitch: +# Treats Strategy order as proximity. This can improve cache +# locality when disabling read repair. Only appropriate for +# single-datacenter deployments. +# +# GossipingPropertyFileSnitch +# This should be your go-to snitch for production use. The rack +# and datacenter for the local node are defined in +# cassandra-rackdc.properties and propagated to other nodes via +# gossip. If cassandra-topology.properties exists, it is used as a +# fallback, allowing migration from the PropertyFileSnitch. +# +# PropertyFileSnitch: +# Proximity is determined by rack and data center, which are +# explicitly configured in cassandra-topology.properties. +# +# Ec2Snitch: +# Appropriate for EC2 deployments in a single Region. Loads Region +# and Availability Zone information from the EC2 API. The Region is +# treated as the datacenter, and the Availability Zone as the rack. +# Only private IPs are used, so this will not work across multiple +# Regions. +# +# Ec2MultiRegionSnitch: +# Uses public IPs as broadcast_address to allow cross-region +# connectivity. (Thus, you should set seed addresses to the public +# IP as well.) You will need to open the storage_port or +# ssl_storage_port on the public IP firewall. (For intra-Region +# traffic, Cassandra will switch to the private IP after +# establishing a connection.) +# +# RackInferringSnitch: +# Proximity is determined by rack and data center, which are +# assumed to correspond to the 3rd and 2nd octet of each node's IP +# address, respectively. Unless this happens to match your +# deployment conventions, this is best used as an example of +# writing a custom Snitch class and is provided in that spirit. +# +# You can use a custom Snitch by setting this to the full class name +# of the snitch, which will be assumed to be on your classpath. +endpoint_snitch: SimpleSnitch + +# controls how often to perform the more expensive part of host score +# calculation +dynamic_snitch_update_interval_in_ms: 100 +# controls how often to reset all host scores, allowing a bad host to +# possibly recover +dynamic_snitch_reset_interval_in_ms: 600000 +# if set greater than zero and read_repair_chance is < 1.0, this will allow +# 'pinning' of replicas to hosts in order to increase cache capacity. +# The badness threshold will control how much worse the pinned host has to be +# before the dynamic snitch will prefer other replicas over it. This is +# expressed as a double which represents a percentage. Thus, a value of +# 0.2 means Cassandra would continue to prefer the static snitch values +# until the pinned host was 20% worse than the fastest. +dynamic_snitch_badness_threshold: 0.1 + +# request_scheduler -- Set this to a class that implements +# RequestScheduler, which will schedule incoming client requests +# according to the specific policy. This is useful for multi-tenancy +# with a single Cassandra cluster. +# NOTE: This is specifically for requests from the client and does +# not affect inter node communication. +# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place +# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of +# client requests to a node with a separate queue for each +# request_scheduler_id. The scheduler is further customized by +# request_scheduler_options as described below. +request_scheduler: org.apache.cassandra.scheduler.NoScheduler + +# Scheduler Options vary based on the type of scheduler +# +# NoScheduler +# Has no options +# +# RoundRobin +# throttle_limit +# The throttle_limit is the number of in-flight +# requests per client. Requests beyond +# that limit are queued up until +# running requests can complete. +# The value of 80 here is twice the number of +# concurrent_reads + concurrent_writes. +# default_weight +# default_weight is optional and allows for +# overriding the default which is 1. +# weights +# Weights are optional and will default to 1 or the +# overridden default_weight. The weight translates into how +# many requests are handled during each turn of the +# RoundRobin, based on the scheduler id. +# +# request_scheduler_options: +# throttle_limit: 80 +# default_weight: 5 +# weights: +# Keyspace1: 1 +# Keyspace2: 5 + +# request_scheduler_id -- An identifier based on which to perform +# the request scheduling. Currently the only valid option is keyspace. +# request_scheduler_id: keyspace + +# Enable or disable inter-node encryption +# JVM defaults for supported SSL socket protocols and cipher suites can +# be replaced using custom encryption options. This is not recommended +# unless you have policies in place that dictate certain settings, or +# need to disable vulnerable ciphers or protocols in case the JVM cannot +# be updated. +# FIPS compliant settings can be configured at JVM level and should not +# involve changing encryption settings here: +# https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/FIPS.html +# *NOTE* No custom encryption options are enabled at the moment +# The available internode options are : all, none, dc, rack +# +# If set to dc cassandra will encrypt the traffic between the DCs +# If set to rack cassandra will encrypt the traffic between the racks +# +# The passwords used in these options must match the passwords used when generating +# the keystore and truststore. For instructions on generating these files, see: +# http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore +# +server_encryption_options: + internode_encryption: none + keystore: conf/.keystore + keystore_password: cassandra + truststore: conf/.truststore + truststore_password: cassandra + # More advanced defaults below: + # protocol: TLS + # algorithm: SunX509 + # store_type: JKS + # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA] + # require_client_auth: false + # require_endpoint_verification: false + +# enable or disable client/server encryption. +client_encryption_options: + enabled: true + # If enabled and optional is set to true encrypted and unencrypted connections are handled. + optional: false + keystore: /certs/server.jks + keystore_password: my_password + require_client_auth: true + # Set trustore and truststore_password if require_client_auth is true + truststore: /certs/truststore.jks + truststore_password: my_password + # More advanced defaults below: + protocol: TLS + store_type: JKS + cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA] + +# internode_compression controls whether traffic between nodes is +# compressed. +# Can be: +# +# all +# all traffic is compressed +# +# dc +# traffic between different datacenters is compressed +# +# none +# nothing is compressed. +internode_compression: dc + +# Enable or disable tcp_nodelay for inter-dc communication. +# Disabling it will result in larger (but fewer) network packets being sent, +# reducing overhead from the TCP protocol itself, at the cost of increasing +# latency if you block for cross-datacenter responses. +inter_dc_tcp_nodelay: false + +# TTL for different trace types used during logging of the repair process. +tracetype_query_ttl: 86400 +tracetype_repair_ttl: 604800 + +# By default, Cassandra logs GC Pauses greater than 200 ms at INFO level +# This threshold can be adjusted to minimize logging if necessary +# gc_log_threshold_in_ms: 200 + +# If unset, all GC Pauses greater than gc_log_threshold_in_ms will log at +# INFO level +# UDFs (user defined functions) are disabled by default. +# As of Cassandra 3.0 there is a sandbox in place that should prevent execution of evil code. +enable_user_defined_functions: false + +# Enables scripted UDFs (JavaScript UDFs). +# Java UDFs are always enabled, if enable_user_defined_functions is true. +# Enable this option to be able to use UDFs with "language javascript" or any custom JSR-223 provider. +# This option has no effect, if enable_user_defined_functions is false. +enable_scripted_user_defined_functions: false + +# Enables materialized view creation on this node. +# Materialized views are considered experimental and are not recommended for production use. +enable_materialized_views: true + +# The default Windows kernel timer and scheduling resolution is 15.6ms for power conservation. +# Lowering this value on Windows can provide much tighter latency and better throughput, however +# some virtualized environments may see a negative performance impact from changing this setting +# below their system default. The sysinternals 'clockres' tool can confirm your system's default +# setting. +windows_timer_interval: 1 + + +# Enables encrypting data at-rest (on disk). Different key providers can be plugged in, but the default reads from +# a JCE-style keystore. A single keystore can hold multiple keys, but the one referenced by +# the "key_alias" is the only key that will be used for encrypt opertaions; previously used keys +# can still (and should!) be in the keystore and will be used on decrypt operations +# (to handle the case of key rotation). +# +# It is strongly recommended to download and install Java Cryptography Extension (JCE) +# Unlimited Strength Jurisdiction Policy Files for your version of the JDK. +# (current link: http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) +# +# Currently, only the following file types are supported for transparent data encryption, although +# more are coming in future cassandra releases: commitlog, hints +transparent_data_encryption_options: + enabled: false + chunk_length_kb: 64 + cipher: AES/CBC/PKCS5Padding + key_alias: testing:1 + # CBC IV length for AES needs to be 16 bytes (which is also the default size) + # iv_length: 16 + key_provider: + - class_name: org.apache.cassandra.security.JKSKeyProvider + parameters: + - keystore: conf/.keystore + keystore_password: cassandra + store_type: JCEKS + key_password: cassandra + + +##################### +# SAFETY THRESHOLDS # +##################### + +# When executing a scan, within or across a partition, we need to keep the +# tombstones seen in memory so we can return them to the coordinator, which +# will use them to make sure other replicas also know about the deleted rows. +# With workloads that generate a lot of tombstones, this can cause performance +# problems and even exaust the server heap. +# (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets) +# Adjust the thresholds here if you understand the dangers and want to +# scan more tombstones anyway. These thresholds may also be adjusted at runtime +# using the StorageService mbean. +tombstone_warn_threshold: 1000 +tombstone_failure_threshold: 100000 + +# Log WARN on any multiple-partition batch size exceeding this value. 5kb per batch by default. +# Caution should be taken on increasing the size of this threshold as it can lead to node instability. +batch_size_warn_threshold_in_kb: 5 + +# Fail any multiple-partition batch exceeding this value. 50kb (10x warn threshold) by default. +batch_size_fail_threshold_in_kb: 50 + +# Log WARN on any batches not of type LOGGED than span across more partitions than this limit +unlogged_batch_across_partitions_warn_threshold: 10 + +# Log a warning when compacting partitions larger than this value +compaction_large_partition_warning_threshold_mb: 100 + +# GC Pauses greater than gc_warn_threshold_in_ms will be logged at WARN level +# Adjust the threshold based on your application throughput requirement +# By default, Cassandra logs GC Pauses greater than 200 ms at INFO level +gc_warn_threshold_in_ms: 1000 + +# Maximum size of any value in SSTables. Safety measure to detect SSTable corruption +# early. Any value size larger than this threshold will result into marking an SSTable +# as corrupted. This should be positive and less than 2048. +# max_value_size_in_mb: 256 + +# Back-pressure settings # +# If enabled, the coordinator will apply the back-pressure strategy specified below to each mutation +# sent to replicas, with the aim of reducing pressure on overloaded replicas. +back_pressure_enabled: false +# The back-pressure strategy applied. +# The default implementation, RateBasedBackPressure, takes three arguments: +# high ratio, factor, and flow type, and uses the ratio between incoming mutation responses and outgoing mutation requests. +# If below high ratio, outgoing mutations are rate limited according to the incoming rate decreased by the given factor; +# if above high ratio, the rate limiting is increased by the given factor; +# such factor is usually best configured between 1 and 10, use larger values for a faster recovery +# at the expense of potentially more dropped mutations; +# the rate limiting is applied according to the flow type: if FAST, it's rate limited at the speed of the fastest replica, +# if SLOW at the speed of the slowest one. +# New strategies can be added. Implementors need to implement org.apache.cassandra.net.BackpressureStrategy and +# provide a public constructor accepting a Map. +back_pressure_strategy: + - class_name: org.apache.cassandra.net.RateBasedBackPressure + parameters: + - high_ratio: 0.90 + factor: 5 + flow: FAST + +# Coalescing Strategies # +# Coalescing multiples messages turns out to significantly boost message processing throughput (think doubling or more). +# On bare metal, the floor for packet processing throughput is high enough that many applications won't notice, but in +# virtualized environments, the point at which an application can be bound by network packet processing can be +# surprisingly low compared to the throughput of task processing that is possible inside a VM. It's not that bare metal +# doesn't benefit from coalescing messages, it's that the number of packets a bare metal network interface can process +# is sufficient for many applications such that no load starvation is experienced even without coalescing. +# There are other benefits to coalescing network messages that are harder to isolate with a simple metric like messages +# per second. By coalescing multiple tasks together, a network thread can process multiple messages for the cost of one +# trip to read from a socket, and all the task submission work can be done at the same time reducing context switching +# and increasing cache friendliness of network message processing. +# See CASSANDRA-8692 for details. + +# Strategy to use for coalescing messages in OutboundTcpConnection. +# Can be fixed, movingaverage, timehorizon, disabled (default). +# You can also specify a subclass of CoalescingStrategies.CoalescingStrategy by name. +# otc_coalescing_strategy: DISABLED + +# How many microseconds to wait for coalescing. For fixed strategy this is the amount of time after the first +# message is received before it will be sent with any accompanying messages. For moving average this is the +# maximum amount of time that will be waited as well as the interval at which messages must arrive on average +# for coalescing to be enabled. +# otc_coalescing_window_us: 200 + +# Do not try to coalesce messages if we already got that many messages. This should be more than 2 and less than 128. +# otc_coalescing_enough_coalesced_messages: 8 + +# How many milliseconds to wait between two expiration runs on the backlog (queue) of the OutboundTcpConnection. +# Expiration is done if messages are piling up in the backlog. Droppable messages are expired to free the memory +# taken by expired messages. The interval should be between 0 and 1000, and in most installations the default value +# will be appropriate. A smaller value could potentially expire messages slightly sooner at the expense of more CPU +# time and queue contention while iterating the backlog of messages. +# An interval of 0 disables any wait time, which is the behavior of former Cassandra versions. +# +# otc_backlog_expiration_interval_ms: 200 diff --git a/.ci/docker-compose-file/certs/README.md b/.ci/docker-compose-file/certs/README.md new file mode 100644 index 000000000..71c389bdd --- /dev/null +++ b/.ci/docker-compose-file/certs/README.md @@ -0,0 +1,23 @@ +Certificate and Key files for testing + +## Cassandra (v3.x) + +### How to convert server PEM to JKS Format + +1. Convert server.crt and server.key to server.p12 + +```bash +openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12 -name "certificate" +``` + +2. Convert server.p12 to server.jks + +```bash +keytool -importkeystore -srckeystore server.p12 -srcstoretype pkcs12 -destkeystore server.jks +``` + +### How to convert CA PEM certificate to truststore.jks + +``` +keytool -import -file ca.pem -keystore truststore.jks +``` diff --git a/.ci/docker-compose-file/certs/client.key b/.ci/docker-compose-file/certs/client.key new file mode 100644 index 000000000..2989d0d78 --- /dev/null +++ b/.ci/docker-compose-file/certs/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAzs74tdftT7xGMGXQSoX/nnFkFAOjNtEVOI3bChzR+w6Xwo8Z +OUiOuOjynKvsJeltdmc0L+cbHZh7j+aHuAqVYxavqaqhFneF0f03t17qju9AixoV +JXgNT3ru56aZFa6Ov6NhfZfRirGnbNrg2RhuNeYZ4TYLH7iMR36exNFP83glXwXM +inMd1tsHL7xHLf3KjCbkusA5ncFWcpIUtpuWVn9aAE402dN7BJWfAbkQ4Y3VToR1 +P/T+W6WBldv0i2WlNbfiuAzuapA3EzJwoyTrG2Qyz7EtXM8XZdOZ6oJmW4s7c4V/ +FBT5knNtmXTt78xBBlIPFas5BAJIeV4eADx9MwIDAQABAoIBAQCZTvcynpJuxIxn +vmItjK5U/4wIBjZNIawQk6BoG7tR2JyJ/1jcjTw4OX/4wr450JRz7MfUJweD5hDb +OTMtLLNXlG6+YR4vsIUEiSlvhy5srVH0jG5Wq2t6mxBVq7vaRd/OkshnuU79+Pq7 +iHqclS7GSACxYkXWyxE6wtPh5aTWP8joK/LvYFiOqKPilUnLZ4hBhmL7CRUCZ0ZA +QGNyEhlmiAL+LNKW2RLXPBxlKX21X78ahUQmkkTM0lBK9x6hm4dD3SpLqmZyQQ9M +UfiMbU6XOYlDva/USZzrvTDlRf9uCG9QOsZzngP1aIy8Cq3QHECOeMIPO9WQLMll +SyY+SpyJAoGBAP4fhnbDpQC6ekd9TNoU9GE/FNNNGKLh82GDgnGcWU/oIzv8GlaR +rkEHTb6aRoPpjTxWIjJpScs9kycC+7N3oNo9rub4s5UvllI+EgQ95+j/5fnZx6gO +la8ousLy1hTYu9C0nTWdTV3YtfC0l0opn7Friv5QafNmhSn74DqrH0BHAoGBANBV +/NhBDAH1PHzYA+XuNLYTLv56Q4osmoen17nPnFNWb1TtWblzb0yWp86GGDFcs8CZ +eH0mXCRUzGMSWtOHe4CbIm2brAYXuL2t6+DZ1A22gsnW5avNrosZRS7eN7BE7DDj +5cp9+Es9UWnArzJU7jSWwAtA6o47WHfHU/pqRB21AoGAGx6eKPqEF2nPNuXmV7e4 +xNAIluw5XtiiMpvoRdubpG1vpS0oWmi9oe73mwm30MgR7Ih8qciWuXvewmENH3/6 +yI+gpMGR2K/1aN166rz4jOMSVfGp3wN/cev00m0774mZsZI03M3mvccs031ST/XV +Nwf1E2Ldi747I9nfeiNc+G0CgYEAslFHD1ntiyd6VGkYPQ978nPM/2dqs7OluILC +tHmslfAfbpOQ/ph9JRK2IqDHyEhOWoWBiazxpO8n2Yx2TSNjZBpkh2h8/uIC7+cT +Q+tuAya6H0ReZISx5sEEZC8zfx4fA2Gs53qWsN+U9W1FB1GGaWC2k2tG1+KXwD3N +9UJLdxkCgYBB96dsfT7nXmy0JLUz0rQ4umBje6H5uvuaevWdVMEptHB+O7+6CAse +OVwqlFLQ4QC7s4/P9FQwfr/0uMRInB1aC043Haa1LbiRcRIlSuBDUezK5xidUbz+ +uB/ABkwwEuqW3Ns1+QieJyyfoNYKZ2v0RtYxBuieKOpUCm3oNFZRWg== +-----END RSA PRIVATE KEY----- diff --git a/.ci/docker-compose-file/certs/client.pem b/.ci/docker-compose-file/certs/client.pem new file mode 100644 index 000000000..454ca4797 --- /dev/null +++ b/.ci/docker-compose-file/certs/client.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEMjCCAhoCFCOrAvLNRztbFFcN0zrCQXoj73cHMA0GCSqGSIb3DQEBCwUAMDQx +EjAQBgNVBAoMCUVNUVggVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNhdGUgQXV0aG9y +aXR5MB4XDTIzMDMxNzA5MzgzMVoXDTMzMDMxNDA5MzgzMVowdzELMAkGA1UEBhMC +U0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYD +VQQKDAlNeU9yZ05hbWUxGDAWBgNVBAsMD015U2VydmljZUNsaWVudDESMBAGA1UE +AwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzs74 +tdftT7xGMGXQSoX/nnFkFAOjNtEVOI3bChzR+w6Xwo8ZOUiOuOjynKvsJeltdmc0 +L+cbHZh7j+aHuAqVYxavqaqhFneF0f03t17qju9AixoVJXgNT3ru56aZFa6Ov6Nh +fZfRirGnbNrg2RhuNeYZ4TYLH7iMR36exNFP83glXwXMinMd1tsHL7xHLf3KjCbk +usA5ncFWcpIUtpuWVn9aAE402dN7BJWfAbkQ4Y3VToR1P/T+W6WBldv0i2WlNbfi +uAzuapA3EzJwoyTrG2Qyz7EtXM8XZdOZ6oJmW4s7c4V/FBT5knNtmXTt78xBBlIP +Fas5BAJIeV4eADx9MwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQBHgfJgMjTgWZXG +eyzIVxaqzWTLxrT7zPy09Mw4qsAl1TfWg9/r8nuskq4bjBQuKm0k9H0HQXz//eFC +Qn85qTHyAmZok6c4ljO2P+kTIl3nkKk5zudmeCTy3W9YBdyWvDXQ/GhbywIfO+1Y +fYA82I5rXVg4c9fUVTNczUFyDNcZzoJoqCS8jwFDtNR0N/fptJN14j8pnYvNV+4c +hZ+pcnhSoz7dD8WjyYCc/QCajJdTyb15i072HxuGmhwltjnwIE/2xfeXCCeUTzsJ +8h4/ABRu9VEqjqDQHepXIflYuVhU38SL0f4ly7neMXmytAbXwGLVM+ME81HG60Bw +8hkfSwKBbEkhUmD6+V1bdUz14I6HjWJt/INtFU+O+MYZbIFt4ep9GKLV3nk97CyL +fwDv5b4WXdC68iWMZqSrADAXr+VG3DgHqpNItj0XmhY6ihmt5tA3Z6IZJj45TShA +vRqTCx3Hf6EO3zf4KCrzaPSSSfVLnGKftA/6oz3bl8EK2e2M44lOspRk4l9k+iBR +sfHPmpiWY0hIiFtd3LD/uGDSBcGkKjU/fLvJZXJpVXwmT9pmK9LzkAPOK1rr97e9 +esHqwe1bo3z7IdeREZ0wdxqGL3BNpm4f1NaIzV/stX+vScau0AyFYXzumjeBIpKa +Gt0A+dZnUfWG6qn5NiRENXxFQSppaA== +-----END CERTIFICATE----- diff --git a/.ci/docker-compose-file/certs/server.jks b/.ci/docker-compose-file/certs/server.jks new file mode 100644 index 000000000..06c2fe184 Binary files /dev/null and b/.ci/docker-compose-file/certs/server.jks differ diff --git a/.ci/docker-compose-file/certs/server.p12 b/.ci/docker-compose-file/certs/server.p12 new file mode 100644 index 000000000..a23d58084 Binary files /dev/null and b/.ci/docker-compose-file/certs/server.p12 differ diff --git a/.ci/docker-compose-file/certs/truststore.jks b/.ci/docker-compose-file/certs/truststore.jks new file mode 100644 index 000000000..5ea593a39 Binary files /dev/null and b/.ci/docker-compose-file/certs/truststore.jks differ diff --git a/.ci/docker-compose-file/docker-compose-cassandra.yaml b/.ci/docker-compose-file/docker-compose-cassandra.yaml new file mode 100644 index 000000000..a54f621c1 --- /dev/null +++ b/.ci/docker-compose-file/docker-compose-cassandra.yaml @@ -0,0 +1,30 @@ +version: '3.9' + +services: + cassandra_server: + container_name: cassandra + build: + context: ./cassandra + args: + CASSANDRA_TAG: ${CASSANDRA_TAG} + image: emqx-cassandra + restart: always + environment: + CASSANDRA_BROADCAST_ADDRESS: "1.2.3.4" + CASSANDRA_RPC_ADDRESS: "0.0.0.0" + volumes: + - ./certs:/certs + #ports: + # - "9042:9042" + # - "9142:9142" + command: + - /bin/bash + - -c + - | + /opt/cassandra/bin/cassandra -f -R > /cassandra.log & + /opt/cassandra/bin/cqlsh -u cassandra -p cassandra -e "CREATE KEYSPACE mqtt WITH REPLICATION = { 'class':'SimpleStrategy','replication_factor':1};" + while [[ $$? -ne 0 ]];do sleep 5; /opt/cassandra/bin/cqlsh -u cassandra -p cassandra -e "CREATE KEYSPACE mqtt WITH REPLICATION = { 'class':'SimpleStrategy','replication_factor':1};"; done + /opt/cassandra/bin/cqlsh -u cassandra -p cassandra -e "describe keyspaces;" + tail -f /cassandra.log + networks: + - emqx_bridge diff --git a/.ci/docker-compose-file/docker-compose-kafka.yaml b/.ci/docker-compose-file/docker-compose-kafka.yaml index d4989bd0b..bbfb4080a 100644 --- a/.ci/docker-compose-file/docker-compose-kafka.yaml +++ b/.ci/docker-compose-file/docker-compose-kafka.yaml @@ -18,7 +18,7 @@ services: - /tmp/emqx-ci/emqx-shared-secret:/var/lib/secret kdc: hostname: kdc.emqx.net - image: ghcr.io/emqx/emqx-builder/5.0-28:1.13.4-24.3.4.2-2-ubuntu20.04 + image: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu20.04 container_name: kdc.emqx.net expose: - 88 # kdc diff --git a/.ci/docker-compose-file/docker-compose-rocketmq.yaml b/.ci/docker-compose-file/docker-compose-rocketmq.yaml new file mode 100644 index 000000000..3c872a7c2 --- /dev/null +++ b/.ci/docker-compose-file/docker-compose-rocketmq.yaml @@ -0,0 +1,34 @@ +version: '3.9' + +services: + mqnamesrv: + image: apache/rocketmq:4.9.4 + container_name: rocketmq_namesrv +# ports: +# - 9876:9876 + volumes: + - ./rocketmq/logs:/opt/logs + - ./rocketmq/store:/opt/store + command: ./mqnamesrv + networks: + - emqx_bridge + + mqbroker: + image: apache/rocketmq:4.9.4 + container_name: rocketmq_broker +# ports: +# - 10909:10909 +# - 10911:10911 + volumes: + - ./rocketmq/logs:/opt/logs + - ./rocketmq/store:/opt/store + - ./rocketmq/conf/broker.conf:/etc/rocketmq/broker.conf + environment: + NAMESRV_ADDR: "rocketmq_namesrv:9876" + JAVA_OPTS: " -Duser.home=/opt" + JAVA_OPT_EXT: "-server -Xms1024m -Xmx1024m -Xmn1024m" + command: ./mqbroker -c /etc/rocketmq/broker.conf + depends_on: + - mqnamesrv + networks: + - emqx_bridge diff --git a/.ci/docker-compose-file/docker-compose-toxiproxy.yaml b/.ci/docker-compose-file/docker-compose-toxiproxy.yaml index 16f18b6c2..9a1d08ba6 100644 --- a/.ci/docker-compose-file/docker-compose-toxiproxy.yaml +++ b/.ci/docker-compose-file/docker-compose-toxiproxy.yaml @@ -22,6 +22,9 @@ services: - 15433:5433 - 16041:6041 - 18000:8000 + - 19876:9876 + - 19042:9042 + - 19142:9142 command: - "-host=0.0.0.0" - "-config=/config/toxiproxy.json" diff --git a/.ci/docker-compose-file/docker-compose.yaml b/.ci/docker-compose-file/docker-compose.yaml index 526c4c182..48d900400 100644 --- a/.ci/docker-compose-file/docker-compose.yaml +++ b/.ci/docker-compose-file/docker-compose.yaml @@ -3,7 +3,7 @@ version: '3.9' services: erlang: container_name: erlang - image: ${DOCKER_CT_RUNNER_IMAGE:-ghcr.io/emqx/emqx-builder/5.0-28:1.13.4-24.3.4.2-2-ubuntu20.04} + image: ${DOCKER_CT_RUNNER_IMAGE:-ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu20.04} env_file: - conf.env environment: diff --git a/.ci/docker-compose-file/rocketmq/conf/broker.conf b/.ci/docker-compose-file/rocketmq/conf/broker.conf new file mode 100644 index 000000000..c343090e4 --- /dev/null +++ b/.ci/docker-compose-file/rocketmq/conf/broker.conf @@ -0,0 +1,22 @@ +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 + +brokerIP1=rocketmq_broker + +defaultTopicQueueNums=4 +autoCreateTopicEnable=true +autoCreateSubscriptionGroup=true + +listenPort=10911 +deleteWhen=04 + +fileReservedTime=120 +mapedFileSizeCommitLog=1073741824 +mapedFileSizeConsumeQueue=300000 +diskMaxUsedSpaceRatio=100 +maxMessageSize=65536 + +brokerRole=ASYNC_MASTER + +flushDiskType=ASYNC_FLUSH diff --git a/.ci/docker-compose-file/rocketmq/logs/.gitkeep b/.ci/docker-compose-file/rocketmq/logs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/.ci/docker-compose-file/rocketmq/store/.gitkeep b/.ci/docker-compose-file/rocketmq/store/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/.ci/docker-compose-file/scripts/run-emqx.sh b/.ci/docker-compose-file/scripts/run-emqx.sh index e8cdfdf4f..8f124aa63 100755 --- a/.ci/docker-compose-file/scripts/run-emqx.sh +++ b/.ci/docker-compose-file/scripts/run-emqx.sh @@ -29,7 +29,7 @@ esac is_node_up() { local node="$1" docker exec -i "$node" \ - bash -c "emqx eval-erl \"['emqx@node1.emqx.io','emqx@node2.emqx.io'] = maps:get(running_nodes, ekka_cluster:info()).\"" > /dev/null 2>&1 + bash -c "emqx eval \"['emqx@node1.emqx.io','emqx@node2.emqx.io'] = maps:get(running_nodes, ekka_cluster:info()).\"" > /dev/null 2>&1 } is_node_listening() { diff --git a/.ci/docker-compose-file/toxiproxy.json b/.ci/docker-compose-file/toxiproxy.json index 2f8c4341b..708cbf1ef 100644 --- a/.ci/docker-compose-file/toxiproxy.json +++ b/.ci/docker-compose-file/toxiproxy.json @@ -77,5 +77,23 @@ "listen": "0.0.0.0:9295", "upstream": "kafka-1.emqx.net:9295", "enabled": true + }, + { + "name": "rocketmq", + "listen": "0.0.0.0:9876", + "upstream": "rocketmq_namesrv:9876", + "enabled": true + }, + { + "name": "cassa_tcp", + "listen": "0.0.0.0:9042", + "upstream": "cassandra:9042", + "enabled": true + }, + { + "name": "cassa_tls", + "listen": "0.0.0.0:9142", + "upstream": "cassandra:9142", + "enabled": true } ] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4de5a83b3..5db0f4465 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -22,5 +22,8 @@ ## CI /deploy/ @emqx/emqx-review-board @Rory-Z +## @Meggielqk owns all files in any i18n directory anywhere in the project +/i18n/ @Meggielqk + ## no owner for changelogs, anyone can approve /changes diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9b96db554..7cb91f0d4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,16 @@ Fixes + + +## Summary +copilot:summary + ## PR Checklist Please convert it to a draft if any of the following conditions are not met. Reviewers may skip over until all the items are checked: - [ ] Added tests for the changes - [ ] Changed lines covered in coverage report -- [ ] Change log has been added to `changes/{ce,ee}/(feat|perf|fix)-.en.md` and `.zh.md` files +- [ ] Change log has been added to `changes/{ce,ee}/(feat|perf|fix)-.en.md` files - [ ] For internal contributor: there is a jira ticket to track this change - [ ] If there should be document changes, a PR to emqx-docs.git is sent, or a jira ticket is created to follow up - [ ] Schema changes are backward compatible diff --git a/.github/workflows/build_and_push_docker_images.yaml b/.github/workflows/build_and_push_docker_images.yaml index adf2c2b84..7391adb5c 100644 --- a/.github/workflows/build_and_push_docker_images.yaml +++ b/.github/workflows/build_and_push_docker_images.yaml @@ -25,7 +25,7 @@ jobs: prepare: runs-on: ubuntu-22.04 # prepare source with any OTP version, no need for a matrix - container: "ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-24.3.4.2-2-ubuntu22.04" + container: "ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu22.04" outputs: PROFILE: ${{ steps.get_profile.outputs.PROFILE }} @@ -121,9 +121,9 @@ jobs: # NOTE: 'otp' and 'elixir' are to configure emqx-builder image # only support latest otp and elixir, not a matrix builder: - - 5.0-32 # update to latest + - 5.0-33 # update to latest otp: - - 24.3.4.2-2 # switch to 25 once ready to release 5.1 + - 24.3.4.2-3 # switch to 25 once ready to release 5.1 elixir: - 'no_elixir' - '1.13.4' # update to latest diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 3141b77d5..2afe23f67 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -24,7 +24,7 @@ jobs: prepare: runs-on: ubuntu-22.04 if: (github.repository_owner == 'emqx' && github.event_name == 'schedule') || github.event_name != 'schedule' - container: ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-24.3.4.2-2-ubuntu22.04 + container: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu22.04 outputs: BUILD_PROFILE: ${{ steps.get_profile.outputs.BUILD_PROFILE }} IS_EXACT_TAG: ${{ steps.get_profile.outputs.IS_EXACT_TAG }} @@ -151,7 +151,7 @@ jobs: profile: - ${{ needs.prepare.outputs.BUILD_PROFILE }} otp: - - 24.3.4.2-2 + - 24.3.4.2-3 os: - macos-11 - macos-12 @@ -203,7 +203,7 @@ jobs: profile: - ${{ needs.prepare.outputs.BUILD_PROFILE }} otp: - - 24.3.4.2-2 + - 24.3.4.2-3 arch: - amd64 - arm64 @@ -221,7 +221,7 @@ jobs: - aws-arm64 - ubuntu-22.04 builder: - - 5.0-32 + - 5.0-33 elixir: - 1.13.4 exclude: @@ -231,19 +231,19 @@ jobs: build_machine: aws-arm64 include: - profile: emqx - otp: 25.1.2-2 + otp: 25.1.2-3 arch: amd64 os: ubuntu22.04 build_machine: ubuntu-22.04 - builder: 5.0-32 + builder: 5.0-33 elixir: 1.13.4 release_with: elixir - profile: emqx - otp: 25.1.2-2 + otp: 25.1.2-3 arch: amd64 os: amzn2 build_machine: ubuntu-22.04 - builder: 5.0-32 + builder: 5.0-33 elixir: 1.13.4 release_with: elixir diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 163956790..d6e4fc961 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -30,12 +30,12 @@ jobs: fail-fast: false matrix: profile: - - ["emqx", "24.3.4.2-2", "el7", "erlang"] - - ["emqx", "25.1.2-2", "ubuntu22.04", "elixir"] - - ["emqx-enterprise", "24.3.4.2-2", "amzn2", "erlang"] - - ["emqx-enterprise", "25.1.2-2", "ubuntu20.04", "erlang"] + - ["emqx", "24.3.4.2-3", "el7", "erlang"] + - ["emqx", "25.1.2-3", "ubuntu22.04", "elixir"] + - ["emqx-enterprise", "24.3.4.2-3", "amzn2", "erlang"] + - ["emqx-enterprise", "25.1.2-3", "ubuntu20.04", "erlang"] builder: - - 5.0-32 + - 5.0-33 elixir: - '1.13.4' @@ -132,7 +132,7 @@ jobs: - emqx - emqx-enterprise otp: - - 24.3.4.2-2 + - 24.3.4.2-3 os: - macos-11 - macos-12-arm64 @@ -165,19 +165,21 @@ jobs: fail-fast: false matrix: profile: - - emqx - - emqx-enterprise + - ["emqx", "5.0.16"] + - ["emqx-enterprise", "5.0.1"] steps: - uses: actions/checkout@v3 - name: prepare run: | - EMQX_NAME=${{ matrix.profile }} + EMQX_NAME=${{ matrix.profile[0] }} PKG_VSN=${PKG_VSN:-$(./pkg-vsn.sh $EMQX_NAME)} EMQX_IMAGE_TAG=emqx/$EMQX_NAME:test + EMQX_IMAGE_OLD_VERSION_TAG=emqx/$EMQX_NAME:${{ matrix.profile[1] }} echo "EMQX_NAME=$EMQX_NAME" >> $GITHUB_ENV echo "PKG_VSN=$PKG_VSN" >> $GITHUB_ENV echo "EMQX_IMAGE_TAG=$EMQX_IMAGE_TAG" >> $GITHUB_ENV + echo "EMQX_IMAGE_OLD_VERSION_TAG=$EMQX_IMAGE_OLD_VERSION_TAG" >> $GITHUB_ENV - uses: docker/setup-buildx-action@v2 - name: build and export to Docker uses: docker/build-push-action@v4 @@ -192,14 +194,24 @@ jobs: run: | CID=$(docker run -d --rm -P $EMQX_IMAGE_TAG) HTTP_PORT=$(docker inspect --format='{{(index (index .NetworkSettings.Ports "18083/tcp") 0).HostPort}}' $CID) + export EMQX_SMOKE_TEST_CHECK_HIDDEN_FIELDS='yes' ./scripts/test/emqx-smoke-test.sh localhost $HTTP_PORT docker stop $CID + - name: test two nodes cluster with proto_dist=inet_tls in docker + run: | + ./scripts/test/start-two-nodes-in-docker.sh -P $EMQX_IMAGE_TAG $EMQX_IMAGE_OLD_VERSION_TAG + HTTP_PORT=$(docker inspect --format='{{(index (index .NetworkSettings.Ports "18083/tcp") 0).HostPort}}' haproxy) + # versions before 5.0.22 have hidden fields included in the API spec + export EMQX_SMOKE_TEST_CHECK_HIDDEN_FIELDS='no' + ./scripts/test/emqx-smoke-test.sh localhost $HTTP_PORT + # cleanup + ./scripts/test/start-two-nodes-in-docker.sh -c - name: export docker image run: | docker save $EMQX_IMAGE_TAG | gzip > $EMQX_NAME-$PKG_VSN.tar.gz - uses: actions/upload-artifact@v3 with: - name: "${{ matrix.profile }}-docker" + name: "${{ matrix.profile[0] }}-docker" path: "${{ env.EMQX_NAME }}-${{ env.PKG_VSN }}.tar.gz" spellcheck: diff --git a/.github/workflows/check_deps_integrity.yaml b/.github/workflows/check_deps_integrity.yaml index 58dd06e30..62dfa24ef 100644 --- a/.github/workflows/check_deps_integrity.yaml +++ b/.github/workflows/check_deps_integrity.yaml @@ -6,7 +6,7 @@ on: jobs: check_deps_integrity: runs-on: ubuntu-22.04 - container: ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-25.1.2-2-ubuntu22.04 + container: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-25.1.2-3-ubuntu22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/code_style_check.yaml b/.github/workflows/code_style_check.yaml index de05f7e59..97c6b0c88 100644 --- a/.github/workflows/code_style_check.yaml +++ b/.github/workflows/code_style_check.yaml @@ -5,7 +5,7 @@ on: [pull_request] jobs: code_style_check: runs-on: ubuntu-22.04 - container: "ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-25.1.2-2-ubuntu22.04" + container: "ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-25.1.2-3-ubuntu22.04" steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/elixir_apps_check.yaml b/.github/workflows/elixir_apps_check.yaml index 181e81305..247f67a8f 100644 --- a/.github/workflows/elixir_apps_check.yaml +++ b/.github/workflows/elixir_apps_check.yaml @@ -9,7 +9,7 @@ jobs: elixir_apps_check: runs-on: ubuntu-22.04 # just use the latest builder - container: "ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-25.1.2-2-ubuntu22.04" + container: "ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-25.1.2-3-ubuntu22.04" strategy: fail-fast: false diff --git a/.github/workflows/elixir_deps_check.yaml b/.github/workflows/elixir_deps_check.yaml index d753693cc..511639a3c 100644 --- a/.github/workflows/elixir_deps_check.yaml +++ b/.github/workflows/elixir_deps_check.yaml @@ -8,7 +8,7 @@ on: jobs: elixir_deps_check: runs-on: ubuntu-22.04 - container: ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-25.1.2-2-ubuntu22.04 + container: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-25.1.2-3-ubuntu22.04 steps: - name: Checkout diff --git a/.github/workflows/elixir_release.yml b/.github/workflows/elixir_release.yml index 1647071af..7bd6102ff 100644 --- a/.github/workflows/elixir_release.yml +++ b/.github/workflows/elixir_release.yml @@ -17,7 +17,7 @@ jobs: profile: - emqx - emqx-enterprise - container: ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-25.1.2-2-ubuntu22.04 + container: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-25.1.2-3-ubuntu22.04 steps: - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/geen_master.yaml b/.github/workflows/geen_master.yaml new file mode 100644 index 000000000..1161ca7d4 --- /dev/null +++ b/.github/workflows/geen_master.yaml @@ -0,0 +1,26 @@ +--- + +name: Keep master green + +on: + schedule: + # run hourly + - cron: "0 * * * *" + workflow_dispatch: + +jobs: + rerun-failed-jobs: + runs-on: ubuntu-22.04 + if: github.repository_owner == 'emqx' + permissions: + checks: read + actions: write + steps: + - uses: actions/checkout@v3 + + - name: run script + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python3 scripts/rerun-failed-checks.py diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2f5ddf171..32a45bd51 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -54,7 +54,7 @@ jobs: OUTPUT_DIR=${{ steps.profile.outputs.s3dir }} aws s3 cp --recursive s3://$BUCKET/$OUTPUT_DIR/${{ github.ref_name }} packages cd packages - DEFAULT_BEAM_PLATFORM='otp24.3.4.2-2' + DEFAULT_BEAM_PLATFORM='otp24.3.4.2-3' # all packages including full-name and default-name are uploaded to s3 # but we only upload default-name packages (and elixir) as github artifacts # so we rename (overwrite) non-default packages before uploading diff --git a/.github/workflows/run_emqx_app_tests.yaml b/.github/workflows/run_emqx_app_tests.yaml index 52ba13373..0a15f6c0b 100644 --- a/.github/workflows/run_emqx_app_tests.yaml +++ b/.github/workflows/run_emqx_app_tests.yaml @@ -12,10 +12,10 @@ jobs: strategy: matrix: builder: - - 5.0-32 + - 5.0-33 otp: - - 24.3.4.2-2 - - 25.1.2-2 + - 24.3.4.2-3 + - 25.1.2-3 # no need to use more than 1 version of Elixir, since tests # run using only Erlang code. This is needed just to specify # the base image. diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index f729c8cbd..bb5aa4a1a 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -17,7 +17,7 @@ jobs: prepare: runs-on: ubuntu-22.04 # prepare source with any OTP version, no need for a matrix - container: ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-24.3.4.2-2-debian11 + container: ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-debian11 steps: - uses: actions/checkout@v3 @@ -50,9 +50,9 @@ jobs: os: - ["debian11", "debian:11-slim"] builder: - - 5.0-32 + - 5.0-33 otp: - - 24.3.4.2-2 + - 24.3.4.2-3 elixir: - 1.13.4 arch: @@ -123,9 +123,9 @@ jobs: os: - ["debian11", "debian:11-slim"] builder: - - 5.0-32 + - 5.0-33 otp: - - 24.3.4.2-2 + - 24.3.4.2-3 elixir: - 1.13.4 arch: diff --git a/.github/workflows/run_relup_tests.yaml b/.github/workflows/run_relup_tests.yaml index cd969045d..8727f4d9d 100644 --- a/.github/workflows/run_relup_tests.yaml +++ b/.github/workflows/run_relup_tests.yaml @@ -15,7 +15,7 @@ concurrency: jobs: relup_test_plan: runs-on: ubuntu-22.04 - container: "ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-24.3.4.2-2-ubuntu22.04" + container: "ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu22.04" outputs: CUR_EE_VSN: ${{ steps.find-versions.outputs.CUR_EE_VSN }} OLD_VERSIONS: ${{ steps.find-versions.outputs.OLD_VERSIONS }} diff --git a/.github/workflows/run_test_cases.yaml b/.github/workflows/run_test_cases.yaml index 1efe7a4e7..8702cd849 100644 --- a/.github/workflows/run_test_cases.yaml +++ b/.github/workflows/run_test_cases.yaml @@ -31,13 +31,13 @@ jobs: MATRIX="$(echo "${APPS}" | jq -c ' [ (.[] | select(.profile == "emqx") | . + { - builder: "5.0-32", - otp: "25.1.2-2", + builder: "5.0-33", + otp: "25.1.2-3", elixir: "1.13.4" }), (.[] | select(.profile == "emqx-enterprise") | . + { - builder: "5.0-32", - otp: ["24.3.4.2-2", "25.1.2-2"][], + builder: "5.0-33", + otp: ["24.3.4.2-3", "25.1.2-3"][], elixir: "1.13.4" }) ] @@ -230,12 +230,12 @@ jobs: - ct - ct_docker runs-on: ubuntu-22.04 - container: "ghcr.io/emqx/emqx-builder/5.0-32:1.13.4-24.3.4.2-2-ubuntu22.04" + container: "ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu22.04" steps: - uses: AutoModality/action-clean@v1 - uses: actions/download-artifact@v3 with: - name: source-emqx-enterprise-24.3.4.2-2 + name: source-emqx-enterprise-24.3.4.2-3 path: . - name: unzip source code run: unzip -q source.zip diff --git a/.tool-versions b/.tool-versions index dcf5945a8..b4d8f8675 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -erlang 24.3.4.2-2 +erlang 24.3.4.2-3 elixir 1.13.4-otp-24 diff --git a/Makefile b/Makefile index 410140616..a5adf0e0a 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ ct: $(REBAR) merge-config static_checks: @$(REBAR) as check do xref, dialyzer @if [ "$${PROFILE}" = 'emqx-enterprise' ]; then $(REBAR) ct --suite apps/emqx/test/emqx_static_checks --readable $(CT_READABLE); fi - @if [ "$${PROFILE}" = 'emqx-enterprise' ]; then ./scripts/check-i18n-style.sh; fi + ./scripts/check-i18n-style.sh APPS=$(shell $(SCRIPTS)/find-apps.sh) @@ -152,6 +152,7 @@ $(PROFILES:%=clean-%): .PHONY: clean-all clean-all: @rm -f rebar.lock + @rm -rf deps @rm -rf _build .PHONY: deps-all diff --git a/README-CN.md b/README-CN.md index 193e5ab98..3eccc267c 100644 --- a/README-CN.md +++ b/README-CN.md @@ -11,9 +11,6 @@ [![YouTube](https://img.shields.io/badge/Subscribe-EMQ%20中文-FF0000?logo=youtube)](https://www.youtube.com/channel/UCir_r04HIsLjf2qqyZ4A8Cg) - -[English](./README.md) | 简体中文 | [русский](./README-RU.md) - EMQX 是一款全球下载量超千万的大规模分布式物联网 MQTT 服务器,单集群支持 1 亿物联网设备连接,消息分发时延低于 1 毫秒。为高可靠、高性能的物联网实时数据移动、处理和集成提供动力,助力企业构建关键业务的 IoT 平台与应用。 EMQX 自 2013 年在 GitHub 发布开源版本以来,获得了来自 50 多个国家和地区的 20000 余家企业用户的广泛认可,累计连接物联网关键设备超过 1 亿台。 diff --git a/README-RU.md b/README-RU.md index fb5ff9608..b8be19e80 100644 --- a/README-RU.md +++ b/README-RU.md @@ -9,7 +9,6 @@ [![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) -[English](./README.md) | [简体中文](./README-CN.md) | русский *EMQX* — это самый масштабируемый и популярный высокопроизводительный MQTT брокер с полностью открытым кодом для интернета вещей, межмашинного взаимодействия и мобильных приложений. EMQX может поддерживать более чем 100 миллионов одновременных соединенией на одном кластере с задержкой в 1 миллисекунду, а также принимать и обрабабывать миллионы MQTT сообщений в секунду. diff --git a/README.md b/README.md index 94baba04f..280371a41 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,6 @@ [![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) - -English | [简体中文](./README-CN.md) | [русский](./README-RU.md) - EMQX is the world's most scalable open-source MQTT broker with a high performance that connects 100M+ IoT devices in 1 cluster, while maintaining 1M message per second throughput and sub-millisecond latency. EMQX supports multiple open standard protocols like MQTT, HTTP, QUIC, and WebSocket. It’s 100% compliant with MQTT 5.0 and 3.x standard, and secures bi-directional communication with MQTT over TLS/SSL and various authentication mechanisms. @@ -25,7 +22,7 @@ For more information, please visit [EMQX homepage](https://www.emqx.io/). ## Get Started -#### EMQX Cloud +#### Run EMQX in the Cloud The simplest way to set up EMQX is to create a managed deployment with EMQX Cloud. You can [try EMQX Cloud for free](https://www.emqx.com/en/signup?utm_source=github.com&utm_medium=referral&utm_campaign=emqx-readme-to-cloud&continue=https://cloud-intl.emqx.com/console/deployments/0?oper=new), no credit card required. @@ -62,6 +59,7 @@ For more organised improvement proposals, you can send pull requests to [EIP](ht ## Get Involved - Follow [@EMQTech on Twitter](https://twitter.com/EMQTech). +- Join our [Slack](https://slack-invite.emqx.io/). - If you have a specific question, check out our [discussion forums](https://github.com/emqx/emqx/discussions). - For general discussions, join us on the [official Discord](https://discord.gg/xYGf3fQnES) team. - Keep updated on [EMQX YouTube](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) by subscribing. diff --git a/apps/emqx/include/emqx_api_lib.hrl b/apps/emqx/include/emqx_api_lib.hrl new file mode 100644 index 000000000..549b0f94c --- /dev/null +++ b/apps/emqx/include/emqx_api_lib.hrl @@ -0,0 +1,36 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-ifndef(EMQX_API_LIB_HRL). +-define(EMQX_API_LIB_HRL, true). + +-define(ERROR_MSG(CODE, REASON), #{code => CODE, message => emqx_misc:readable_error_msg(REASON)}). + +-define(OK(CONTENT), {200, CONTENT}). + +-define(NO_CONTENT, 204). + +-define(BAD_REQUEST(CODE, REASON), {400, ?ERROR_MSG(CODE, REASON)}). +-define(BAD_REQUEST(REASON), ?BAD_REQUEST('BAD_REQUEST', REASON)). + +-define(NOT_FOUND(REASON), {404, ?ERROR_MSG('NOT_FOUND', REASON)}). + +-define(INTERNAL_ERROR(REASON), {500, ?ERROR_MSG('INTERNAL_ERROR', REASON)}). + +-define(NOT_IMPLEMENTED, 501). + +-define(SERVICE_UNAVAILABLE(REASON), {503, ?ERROR_MSG('SERVICE_UNAVAILABLE', REASON)}). +-endif. diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index a781a8a5a..9079322eb 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -26,10 +26,10 @@ {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}, {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}}, - {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}}, - {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.5"}}}, + {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.6"}}}, + {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.6"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, - {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.37.2"}}}, + {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.38.0"}}}, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}, @@ -59,4 +59,12 @@ {statistics, true} ]}. -{project_plugins, [erlfmt]}. +{project_plugins, [ + {erlfmt, [ + {files, [ + "{src,include,test}/*.{hrl,erl,app.src}", + "rebar.config", + "rebar.config.script" + ]} + ]} +]}. diff --git a/apps/emqx/rebar.config.script b/apps/emqx/rebar.config.script index 0827570ff..7aadb1f59 100644 --- a/apps/emqx/rebar.config.script +++ b/apps/emqx/rebar.config.script @@ -24,20 +24,20 @@ IsQuicSupp = fun() -> end, Bcrypt = {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {tag, "0.6.0"}}}, -Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.113"}}}. +Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.114"}}}. Dialyzer = fun(Config) -> - {dialyzer, OldDialyzerConfig} = lists:keyfind(dialyzer, 1, Config), - {plt_extra_apps, OldExtra} = lists:keyfind(plt_extra_apps, 1, OldDialyzerConfig), - Extra = OldExtra ++ [quicer || IsQuicSupp()], - NewDialyzerConfig = [{plt_extra_apps, Extra} | OldDialyzerConfig], - lists:keystore( - dialyzer, - 1, - Config, - {dialyzer, NewDialyzerConfig} - ) - end. + {dialyzer, OldDialyzerConfig} = lists:keyfind(dialyzer, 1, Config), + {plt_extra_apps, OldExtra} = lists:keyfind(plt_extra_apps, 1, OldDialyzerConfig), + Extra = OldExtra ++ [quicer || IsQuicSupp()], + NewDialyzerConfig = [{plt_extra_apps, Extra} | OldDialyzerConfig], + lists:keystore( + dialyzer, + 1, + Config, + {dialyzer, NewDialyzerConfig} + ) +end. ExtraDeps = fun(C) -> {deps, Deps0} = lists:keyfind(deps, 1, C), diff --git a/apps/emqx/src/emqx_api_lib.erl b/apps/emqx/src/emqx_api_lib.erl new file mode 100644 index 000000000..8c49c57c3 --- /dev/null +++ b/apps/emqx/src/emqx_api_lib.erl @@ -0,0 +1,69 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_api_lib). + +-export([ + with_node/2, + with_node_or_cluster/2 +]). + +-include("emqx_api_lib.hrl"). + +-define(NODE_NOT_FOUND(NODE), ?NOT_FOUND(<<"Node not found: ", NODE/binary>>)). + +%%-------------------------------------------------------------------- +%% exported API +%%-------------------------------------------------------------------- +-spec with_node(binary(), fun((atom()) -> {ok, term()} | {error, term()})) -> + ?OK(term()) | ?NOT_FOUND(binary()) | ?BAD_REQUEST(term()). +with_node(BinNode, Fun) -> + case lookup_node(BinNode) of + {ok, Node} -> + handle_result(Fun(Node)); + not_found -> + ?NODE_NOT_FOUND(BinNode) + end. + +-spec with_node_or_cluster(binary(), fun((atom()) -> {ok, term()} | {error, term()})) -> + ?OK(term()) | ?NOT_FOUND(iolist()) | ?BAD_REQUEST(term()). +with_node_or_cluster(<<"all">>, Fun) -> + handle_result(Fun(all)); +with_node_or_cluster(Node, Fun) -> + with_node(Node, Fun). + +%%-------------------------------------------------------------------- +%% Internal +%%-------------------------------------------------------------------- + +-spec lookup_node(binary()) -> {ok, atom()} | not_found. +lookup_node(BinNode) -> + case emqx_misc:safe_to_existing_atom(BinNode, utf8) of + {ok, Node} -> + case lists:member(Node, mria:running_nodes()) of + true -> + {ok, Node}; + false -> + not_found + end; + _Error -> + not_found + end. + +handle_result({ok, Result}) -> + ?OK(Result); +handle_result({error, Reason}) -> + ?BAD_REQUEST(Reason). diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index 6188d8030..77ece1c60 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -72,9 +72,13 @@ set_init_config_load_done() -> get_init_config_load_done() -> application:get_env(emqx, init_config_load_done, false). +%% @doc Set the transaction id from which this node should start applying after boot. +%% The transaction ID is received from the core node which we just copied the latest +%% config from. set_init_tnx_id(TnxId) -> application:set_env(emqx, cluster_rpc_init_tnx_id, TnxId). +%% @doc Get the transaction id from which this node should start applying after boot. get_init_tnx_id() -> application:get_env(emqx, cluster_rpc_init_tnx_id, -1). diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 9acad4d57..29a59e482 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -276,7 +276,9 @@ init( ), {NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo), #channel{ - conninfo = NConnInfo, + %% We remove the peercert because it duplicates to what's stored in the socket, + %% Saving a copy here causes unnecessary wast of memory (about 1KB per connection). + conninfo = maps:put(peercert, undefined, NConnInfo), clientinfo = NClientInfo, topic_aliases = #{ inbound => #{}, @@ -2128,17 +2130,23 @@ publish_will_msg( ClientInfo = #{mountpoint := MountPoint}, Msg = #message{topic = Topic} ) -> - case emqx_access_control:authorize(ClientInfo, publish, Topic) of - allow -> - NMsg = emqx_mountpoint:mount(MountPoint, Msg), - _ = emqx_broker:publish(NMsg), - ok; - deny -> + PublishingDisallowed = emqx_access_control:authorize(ClientInfo, publish, Topic) =/= allow, + ClientBanned = emqx_banned:check(ClientInfo), + case PublishingDisallowed orelse ClientBanned of + true -> ?tp( warning, last_will_testament_publish_denied, - #{topic => Topic} + #{ + topic => Topic, + client_banned => ClientBanned, + publishing_disallowed => PublishingDisallowed + } ), + ok; + false -> + NMsg = emqx_mountpoint:mount(MountPoint, Msg), + _ = emqx_broker:publish(NMsg), ok end. diff --git a/apps/emqx/src/emqx_cm.erl b/apps/emqx/src/emqx_cm.erl index 6de05dabe..f8c510482 100644 --- a/apps/emqx/src/emqx_cm.erl +++ b/apps/emqx/src/emqx_cm.erl @@ -465,23 +465,23 @@ request_stepdown(Action, ConnMod, Pid) -> catch % emqx_ws_connection: call _:noproc -> - ok = ?tp(debug, "session_already_gone", #{pid => Pid, action => Action}), + ok = ?tp(debug, "session_already_gone", #{stale_pid => Pid, action => Action}), {error, noproc}; % emqx_connection: gen_server:call _:{noproc, _} -> - ok = ?tp(debug, "session_already_gone", #{pid => Pid, action => Action}), + ok = ?tp(debug, "session_already_gone", #{stale_pid => Pid, action => Action}), {error, noproc}; _:{shutdown, _} -> - ok = ?tp(debug, "session_already_shutdown", #{pid => Pid, action => Action}), + ok = ?tp(debug, "session_already_shutdown", #{stale_pid => Pid, action => Action}), {error, noproc}; _:{{shutdown, _}, _} -> - ok = ?tp(debug, "session_already_shutdown", #{pid => Pid, action => Action}), + ok = ?tp(debug, "session_already_shutdown", #{stale_pid => Pid, action => Action}), {error, noproc}; _:{timeout, {gen_server, call, _}} -> ?tp( warning, "session_stepdown_request_timeout", - #{pid => Pid, action => Action, stale_channel => stale_channel_info(Pid)} + #{stale_pid => Pid, action => Action, stale_channel => stale_channel_info(Pid)} ), ok = force_kill(Pid), {error, timeout}; @@ -490,7 +490,7 @@ request_stepdown(Action, ConnMod, Pid) -> error, "session_stepdown_request_exception", #{ - pid => Pid, + stale_pid => Pid, action => Action, reason => Error, stacktrace => St, @@ -671,7 +671,7 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{chan_pmon := PMon}) -> - ?tp(emqx_cm_process_down, #{pid => Pid, reason => _Reason}), + ?tp(emqx_cm_process_down, #{stale_pid => Pid, reason => _Reason}), ChanPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)], {Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon), lists:foreach(fun mark_channel_disconnected/1, ChanPids), diff --git a/apps/emqx/src/emqx_crl_cache.erl b/apps/emqx/src/emqx_crl_cache.erl new file mode 100644 index 000000000..79e47a6dc --- /dev/null +++ b/apps/emqx/src/emqx_crl_cache.erl @@ -0,0 +1,314 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% @doc EMQX CRL cache. +%%-------------------------------------------------------------------- + +-module(emqx_crl_cache). + +%% API +-export([ + start_link/0, + start_link/1, + register_der_crls/2, + refresh/1, + evict/1 +]). + +%% gen_server callbacks +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2 +]). + +%% internal exports +-export([http_get/2]). + +-behaviour(gen_server). + +-include("logger.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +-define(HTTP_TIMEOUT, timer:seconds(15)). +-define(RETRY_TIMEOUT, 5_000). +-ifdef(TEST). +-define(MIN_REFRESH_PERIOD, timer:seconds(5)). +-else. +-define(MIN_REFRESH_PERIOD, timer:minutes(1)). +-endif. +-define(DEFAULT_REFRESH_INTERVAL, timer:minutes(15)). +-define(DEFAULT_CACHE_CAPACITY, 100). + +-record(state, { + refresh_timers = #{} :: #{binary() => timer:tref()}, + refresh_interval = timer:minutes(15) :: timer:time(), + http_timeout = ?HTTP_TIMEOUT :: timer:time(), + %% keeps track of URLs by insertion time + insertion_times = gb_trees:empty() :: gb_trees:tree(timer:time(), url()), + %% the set of cached URLs, for testing if an URL is already + %% registered. + cached_urls = sets:new([{version, 2}]) :: sets:set(url()), + cache_capacity = 100 :: pos_integer(), + %% for future use + extra = #{} :: map() +}). +-type url() :: uri_string:uri_string(). +-type state() :: #state{}. + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +start_link() -> + Config = gather_config(), + start_link(Config). + +start_link(Config = #{cache_capacity := _, refresh_interval := _, http_timeout := _}) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, Config, []). + +-spec refresh(url()) -> ok. +refresh(URL) -> + gen_server:cast(?MODULE, {refresh, URL}). + +-spec evict(url()) -> ok. +evict(URL) -> + gen_server:cast(?MODULE, {evict, URL}). + +%% Adds CRLs in DER format to the cache and register them for periodic +%% refresh. +-spec register_der_crls(url(), [public_key:der_encoded()]) -> ok. +register_der_crls(URL, CRLs) when is_list(CRLs) -> + gen_server:cast(?MODULE, {register_der_crls, URL, CRLs}). + +%%-------------------------------------------------------------------- +%% gen_server behaviour +%%-------------------------------------------------------------------- + +init(Config) -> + #{ + cache_capacity := CacheCapacity, + refresh_interval := RefreshIntervalMS, + http_timeout := HTTPTimeoutMS + } = Config, + State = #state{ + cache_capacity = CacheCapacity, + refresh_interval = RefreshIntervalMS, + http_timeout = HTTPTimeoutMS + }, + {ok, State}. + +handle_call(Call, _From, State) -> + {reply, {error, {bad_call, Call}}, State}. + +handle_cast({evict, URL}, State0 = #state{refresh_timers = RefreshTimers0}) -> + emqx_ssl_crl_cache:delete(URL), + MTimer = maps:get(URL, RefreshTimers0, undefined), + emqx_misc:cancel_timer(MTimer), + RefreshTimers = maps:without([URL], RefreshTimers0), + State = State0#state{refresh_timers = RefreshTimers}, + ?tp( + crl_cache_evict, + #{url => URL} + ), + {noreply, State}; +handle_cast({register_der_crls, URL, CRLs}, State0) -> + handle_register_der_crls(State0, URL, CRLs); +handle_cast({refresh, URL}, State0) -> + case do_http_fetch_and_cache(URL, State0#state.http_timeout) of + {error, Error} -> + ?tp(crl_refresh_failure, #{error => Error, url => URL}), + ?SLOG(error, #{ + msg => "failed_to_fetch_crl_response", + url => URL, + error => Error + }), + {noreply, ensure_timer(URL, State0, ?RETRY_TIMEOUT)}; + {ok, _CRLs} -> + ?SLOG(debug, #{ + msg => "fetched_crl_response", + url => URL + }), + {noreply, ensure_timer(URL, State0)} + end; +handle_cast(_Cast, State) -> + {noreply, State}. + +handle_info( + {timeout, TRef, {refresh, URL}}, + State = #state{ + refresh_timers = RefreshTimers, + http_timeout = HTTPTimeoutMS + } +) -> + case maps:get(URL, RefreshTimers, undefined) of + TRef -> + ?tp(debug, crl_refresh_timer, #{url => URL}), + case do_http_fetch_and_cache(URL, HTTPTimeoutMS) of + {error, Error} -> + ?SLOG(error, #{ + msg => "failed_to_fetch_crl_response", + url => URL, + error => Error + }), + {noreply, ensure_timer(URL, State, ?RETRY_TIMEOUT)}; + {ok, _CRLs} -> + ?tp(debug, crl_refresh_timer_done, #{url => URL}), + {noreply, ensure_timer(URL, State)} + end; + _ -> + {noreply, State} + end; +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% internal functions +%%-------------------------------------------------------------------- + +http_get(URL, HTTPTimeout) -> + httpc:request( + get, + {URL, [{"connection", "close"}]}, + [{timeout, HTTPTimeout}], + [{body_format, binary}] + ). + +do_http_fetch_and_cache(URL, HTTPTimeoutMS) -> + ?tp(crl_http_fetch, #{crl_url => URL}), + Resp = ?MODULE:http_get(URL, HTTPTimeoutMS), + case Resp of + {ok, {{_, 200, _}, _, Body}} -> + case parse_crls(Body) of + error -> + {error, invalid_crl}; + CRLs -> + %% Note: must ensure it's a string and not a + %% binary because that's what the ssl manager uses + %% when doing lookups. + emqx_ssl_crl_cache:insert(to_string(URL), {der, CRLs}), + ?tp(crl_cache_insert, #{url => URL, crls => CRLs}), + {ok, CRLs} + end; + {ok, {{_, Code, _}, _, Body}} -> + {error, {bad_response, #{code => Code, body => Body}}}; + {error, Error} -> + {error, {http_error, Error}} + end. + +parse_crls(Bin) -> + try + [CRL || {'CertificateList', CRL, not_encrypted} <- public_key:pem_decode(Bin)] + catch + _:_ -> + error + end. + +ensure_timer(URL, State = #state{refresh_interval = Timeout}) -> + ensure_timer(URL, State, Timeout). + +ensure_timer(URL, State = #state{refresh_timers = RefreshTimers0}, Timeout) -> + ?tp(crl_cache_ensure_timer, #{url => URL, timeout => Timeout}), + MTimer = maps:get(URL, RefreshTimers0, undefined), + emqx_misc:cancel_timer(MTimer), + RefreshTimers = RefreshTimers0#{ + URL => emqx_misc:start_timer( + Timeout, + {refresh, URL} + ) + }, + State#state{refresh_timers = RefreshTimers}. + +-spec gather_config() -> + #{ + cache_capacity := pos_integer(), + refresh_interval := timer:time(), + http_timeout := timer:time() + }. +gather_config() -> + %% TODO: add a config handler to refresh the config when those + %% globals change? + CacheCapacity = emqx_config:get([crl_cache, capacity], ?DEFAULT_CACHE_CAPACITY), + RefreshIntervalMS0 = emqx_config:get([crl_cache, refresh_interval], ?DEFAULT_REFRESH_INTERVAL), + MinimumRefreshInverval = ?MIN_REFRESH_PERIOD, + RefreshIntervalMS = max(RefreshIntervalMS0, MinimumRefreshInverval), + HTTPTimeoutMS = emqx_config:get([crl_cache, http_timeout], ?HTTP_TIMEOUT), + #{ + cache_capacity => CacheCapacity, + refresh_interval => RefreshIntervalMS, + http_timeout => HTTPTimeoutMS + }. + +-spec handle_register_der_crls(state(), url(), [public_key:der_encoded()]) -> {noreply, state()}. +handle_register_der_crls(State0, URL0, CRLs) -> + #state{cached_urls = CachedURLs0} = State0, + URL = to_string(URL0), + case sets:is_element(URL, CachedURLs0) of + true -> + {noreply, State0}; + false -> + emqx_ssl_crl_cache:insert(URL, {der, CRLs}), + ?tp(debug, new_crl_url_inserted, #{url => URL}), + State1 = do_register_url(State0, URL), + State2 = handle_cache_overflow(State1), + State = ensure_timer(URL, State2), + {noreply, State} + end. + +-spec do_register_url(state(), url()) -> state(). +do_register_url(State0, URL) -> + #state{ + cached_urls = CachedURLs0, + insertion_times = InsertionTimes0 + } = State0, + Now = erlang:monotonic_time(), + CachedURLs = sets:add_element(URL, CachedURLs0), + InsertionTimes = gb_trees:enter(Now, URL, InsertionTimes0), + State0#state{ + cached_urls = CachedURLs, + insertion_times = InsertionTimes + }. + +-spec handle_cache_overflow(state()) -> state(). +handle_cache_overflow(State0) -> + #state{ + cached_urls = CachedURLs0, + insertion_times = InsertionTimes0, + cache_capacity = CacheCapacity, + refresh_timers = RefreshTimers0 + } = State0, + case sets:size(CachedURLs0) > CacheCapacity of + false -> + State0; + true -> + {_Time, OldestURL, InsertionTimes} = gb_trees:take_smallest(InsertionTimes0), + emqx_ssl_crl_cache:delete(OldestURL), + MTimer = maps:get(OldestURL, RefreshTimers0, undefined), + emqx_misc:cancel_timer(MTimer), + RefreshTimers = maps:remove(OldestURL, RefreshTimers0), + CachedURLs = sets:del_element(OldestURL, CachedURLs0), + ?tp(debug, crl_cache_overflow, #{oldest_url => OldestURL}), + State0#state{ + insertion_times = InsertionTimes, + cached_urls = CachedURLs, + refresh_timers = RefreshTimers + } + end. + +to_string(B) when is_binary(B) -> + binary_to_list(B); +to_string(L) when is_list(L) -> + L. diff --git a/apps/emqx/src/emqx_kernel_sup.erl b/apps/emqx/src/emqx_kernel_sup.erl index 9d2f71068..1027ef639 100644 --- a/apps/emqx/src/emqx_kernel_sup.erl +++ b/apps/emqx/src/emqx_kernel_sup.erl @@ -36,7 +36,8 @@ init([]) -> child_spec(emqx_stats, worker), child_spec(emqx_metrics, worker), child_spec(emqx_authn_authz_metrics_sup, supervisor), - child_spec(emqx_ocsp_cache, worker) + child_spec(emqx_ocsp_cache, worker), + child_spec(emqx_crl_cache, worker) ] }}. diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 97bc15ad3..4e5843166 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -388,7 +388,11 @@ do_start_listener(quic, ListenerName, #{bind := Bind} = Opts) -> ] ++ case maps:get(cacertfile, SSLOpts, undefined) of undefined -> []; - CaCertFile -> [{cacertfile, binary_to_list(CaCertFile)}] + CaCertFile -> [{cacertfile, str(CaCertFile)}] + end ++ + case maps:get(password, SSLOpts, undefined) of + undefined -> []; + Password -> [{password, str(Password)}] end ++ optional_quic_listener_opts(Opts), ConnectionOpts = #{ @@ -487,7 +491,8 @@ esockd_opts(ListenerId, Type, Opts0) -> tcp -> Opts3#{tcp_options => tcp_opts(Opts0)}; ssl -> - OptsWithSNI = inject_sni_fun(ListenerId, Opts0), + OptsWithCRL = inject_crl_config(Opts0), + OptsWithSNI = inject_sni_fun(ListenerId, OptsWithCRL), SSLOpts = ssl_opts(OptsWithSNI), Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)} end @@ -794,3 +799,17 @@ inject_sni_fun(ListenerId, Conf = #{ssl_options := #{ocsp := #{enable_ocsp_stapl emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf); inject_sni_fun(_ListenerId, Conf) -> Conf. + +inject_crl_config( + Conf = #{ssl_options := #{enable_crl_check := true} = SSLOpts} +) -> + HTTPTimeout = emqx_config:get([crl_cache, http_timeout], timer:seconds(15)), + Conf#{ + ssl_options := SSLOpts#{ + %% `crl_check => true' doesn't work + crl_check => peer, + crl_cache => {emqx_ssl_crl_cache, {internal, [{http, HTTPTimeout}]}} + } + }; +inject_crl_config(Conf) -> + Conf. diff --git a/apps/emqx/src/emqx_misc.erl b/apps/emqx/src/emqx_misc.erl index 18ecc644a..cdd62df11 100644 --- a/apps/emqx/src/emqx_misc.erl +++ b/apps/emqx/src/emqx_misc.erl @@ -545,10 +545,23 @@ readable_error_msg(Error) -> {ok, Msg} -> Msg; false -> - iolist_to_binary(io_lib:format("~0p", [Error])) + to_hr_error(Error) end end. +to_hr_error(nxdomain) -> + <<"Could not resolve host">>; +to_hr_error(econnrefused) -> + <<"Connection refused">>; +to_hr_error({unauthorized_client, _}) -> + <<"Unauthorized client">>; +to_hr_error({not_authorized, _}) -> + <<"Not authorized">>; +to_hr_error({malformed_username_or_password, _}) -> + <<"Bad username or password">>; +to_hr_error(Error) -> + iolist_to_binary(io_lib:format("~0p", [Error])). + try_to_existing_atom(Convert, Data, Encoding) -> try Convert(Data, Encoding) of Atom -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 23583ead4..0f90677bd 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -226,6 +226,11 @@ roots(low) -> sc( ref("trace"), #{} + )}, + {"crl_cache", + sc( + ref("crl_cache"), + #{importance => ?IMPORTANCE_HIDDEN} )} ]. @@ -794,6 +799,37 @@ fields("listeners") -> } )} ]; +fields("crl_cache") -> + %% Note: we make the refresh interval and HTTP timeout global (not + %% per-listener) because multiple SSL listeners might point to the + %% same URL. If they had diverging timeout options, it would be + %% confusing. + [ + {"refresh_interval", + sc( + duration(), + #{ + default => <<"15m">>, + desc => ?DESC("crl_cache_refresh_interval") + } + )}, + {"http_timeout", + sc( + duration(), + #{ + default => <<"15s">>, + desc => ?DESC("crl_cache_refresh_http_timeout") + } + )}, + {"capacity", + sc( + pos_integer(), + #{ + default => 100, + desc => ?DESC("crl_cache_capacity") + } + )} + ]; fields("mqtt_tcp_listener") -> mqtt_listener(1883) ++ [ @@ -1456,7 +1492,7 @@ fields("broker") -> {"perf", sc( ref("broker_perf"), - #{} + #{importance => ?IMPORTANCE_HIDDEN} )}, {"shared_subscription_group", sc( @@ -2065,6 +2101,8 @@ desc("shared_subscription_group") -> "Per group dispatch strategy for shared subscription"; desc("ocsp") -> "Per listener OCSP Stapling configuration."; +desc("crl_cache") -> + "Global CRL cache options."; desc(_) -> undefined. @@ -2261,16 +2299,25 @@ server_ssl_opts_schema(Defaults, IsRanchListener) -> #{ required => false, %% TODO: remove after e5.0.2 - hidden => true, + importance => ?IMPORTANCE_HIDDEN, validator => fun ocsp_inner_validator/1 } + )}, + {"enable_crl_check", + sc( + boolean(), + #{ + default => false, + desc => ?DESC("server_ssl_opts_schema_enable_crl_check") + } )} ] ]. mqtt_ssl_listener_ssl_options_validator(Conf) -> Checks = [ - fun ocsp_outer_validator/1 + fun ocsp_outer_validator/1, + fun crl_outer_validator/1 ], case emqx_misc:pipeline(Checks, Conf, not_used) of {ok, _, _} -> @@ -2305,6 +2352,18 @@ ocsp_inner_validator(#{<<"enable_ocsp_stapling">> := true} = Conf) -> ), ok. +crl_outer_validator( + #{<<"enable_crl_check">> := true} = SSLOpts +) -> + case maps:get(<<"verify">>, SSLOpts) of + verify_peer -> + ok; + _ -> + {error, "verify must be verify_peer when CRL check is enabled"} + end; +crl_outer_validator(_SSLOpts) -> + ok. + %% @doc Make schema for SSL client. -spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema(). client_ssl_opts_schema(Defaults) -> @@ -2938,7 +2997,7 @@ quic_feature_toggle(Desc) -> typerefl:alias("boolean", typerefl:union([true, false, 0, 1])), #{ desc => Desc, - hidden => true, + importance => ?IMPORTANCE_HIDDEN, required => false, converter => fun (true) -> 1; @@ -2953,7 +3012,7 @@ quic_lowlevel_settings_uint(Low, High, Desc) -> range(Low, High), #{ required => false, - hidden => true, + importance => ?IMPORTANCE_HIDDEN, desc => Desc } ). @@ -2964,9 +3023,9 @@ is_quic_ssl_opts(Name) -> "cacertfile", "certfile", "keyfile", - "verify" + "verify", + "password" %% Followings are planned - %% , "password" %% , "hibernate_after" %% , "fail_if_no_peer_cert" %% , "handshake_timeout" diff --git a/apps/emqx/src/emqx_ssl_crl_cache.erl b/apps/emqx/src/emqx_ssl_crl_cache.erl new file mode 100644 index 000000000..13eccbd83 --- /dev/null +++ b/apps/emqx/src/emqx_ssl_crl_cache.erl @@ -0,0 +1,237 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2015-2022. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% + +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +%---------------------------------------------------------------------- +% Based on `otp/lib/ssl/src/ssl_crl_cache.erl' +%---------------------------------------------------------------------- + +%---------------------------------------------------------------------- +%% Purpose: Simple default CRL cache +%%---------------------------------------------------------------------- + +-module(emqx_ssl_crl_cache). + +-include_lib("ssl/src/ssl_internal.hrl"). +-include_lib("public_key/include/public_key.hrl"). + +-behaviour(ssl_crl_cache_api). + +-export_type([crl_src/0, uri/0]). +-type crl_src() :: {file, file:filename()} | {der, public_key:der_encoded()}. +-type uri() :: uri_string:uri_string(). + +-export([lookup/3, select/2, fresh_crl/2]). +-export([insert/1, insert/2, delete/1]). + +%% Allow usage of OTP certificate record fields (camelCase). +-elvis([ + {elvis_style, atom_naming_convention, #{ + regex => "^([a-z][a-z0-9]*_?)([a-zA-Z0-9]*_?)*$", + enclosed_atoms => ".*" + }} +]). + +%%==================================================================== +%% Cache callback API +%%==================================================================== + +lookup( + #'DistributionPoint'{distributionPoint = {fullName, Names}}, + _Issuer, + CRLDbInfo +) -> + get_crls(Names, CRLDbInfo); +lookup(_, _, _) -> + not_available. + +select(GenNames, CRLDbHandle) when is_list(GenNames) -> + lists:flatmap( + fun + ({directoryName, Issuer}) -> + select(Issuer, CRLDbHandle); + (_) -> + [] + end, + GenNames + ); +select(Issuer, {{_Cache, Mapping}, _}) -> + case ssl_pkix_db:lookup(Issuer, Mapping) of + undefined -> + []; + CRLs -> + CRLs + end. + +fresh_crl(#'DistributionPoint'{distributionPoint = {fullName, Names}}, CRL) -> + case get_crls(Names, undefined) of + not_available -> + CRL; + NewCRL -> + NewCRL + end. + +%%==================================================================== +%% API +%%==================================================================== + +insert(CRLs) -> + insert(?NO_DIST_POINT, CRLs). + +insert(URI, {file, File}) when is_list(URI) -> + case file:read_file(File) of + {ok, PemBin} -> + PemEntries = public_key:pem_decode(PemBin), + CRLs = [ + CRL + || {'CertificateList', CRL, not_encrypted} <- + PemEntries + ], + do_insert(URI, CRLs); + Error -> + Error + end; +insert(URI, {der, CRLs}) -> + do_insert(URI, CRLs). + +delete({file, File}) -> + case file:read_file(File) of + {ok, PemBin} -> + PemEntries = public_key:pem_decode(PemBin), + CRLs = [ + CRL + || {'CertificateList', CRL, not_encrypted} <- + PemEntries + ], + ssl_manager:delete_crls({?NO_DIST_POINT, CRLs}); + Error -> + Error + end; +delete({der, CRLs}) -> + ssl_manager:delete_crls({?NO_DIST_POINT, CRLs}); +delete(URI) -> + case uri_string:normalize(URI, [return_map]) of + #{scheme := "http", path := Path} -> + ssl_manager:delete_crls(string:trim(Path, leading, "/")); + _ -> + {error, {only_http_distribution_points_supported, URI}} + end. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +do_insert(URI, CRLs) -> + case uri_string:normalize(URI, [return_map]) of + #{scheme := "http", path := Path} -> + ssl_manager:insert_crls(string:trim(Path, leading, "/"), CRLs); + _ -> + {error, {only_http_distribution_points_supported, URI}} + end. + +get_crls([], _) -> + not_available; +get_crls( + [{uniformResourceIdentifier, "http" ++ _ = URL} | Rest], + CRLDbInfo +) -> + case cache_lookup(URL, CRLDbInfo) of + [] -> + handle_http(URL, Rest, CRLDbInfo); + CRLs -> + CRLs + end; +get_crls([_ | Rest], CRLDbInfo) -> + %% unsupported CRL location + get_crls(Rest, CRLDbInfo). + +http_lookup(URL, Rest, CRLDbInfo, Timeout) -> + case application:ensure_started(inets) of + ok -> + http_get(URL, Rest, CRLDbInfo, Timeout); + _ -> + get_crls(Rest, CRLDbInfo) + end. + +http_get(URL, Rest, CRLDbInfo, Timeout) -> + case emqx_crl_cache:http_get(URL, Timeout) of + {ok, {_Status, _Headers, Body}} -> + case Body of + <<"-----BEGIN", _/binary>> -> + Pem = public_key:pem_decode(Body), + CRLs = lists:filtermap( + fun + ({'CertificateList', CRL, not_encrypted}) -> + {true, CRL}; + (_) -> + false + end, + Pem + ), + emqx_crl_cache:register_der_crls(URL, CRLs), + CRLs; + _ -> + try public_key:der_decode('CertificateList', Body) of + _ -> + CRLs = [Body], + emqx_crl_cache:register_der_crls(URL, CRLs), + CRLs + catch + _:_ -> + get_crls(Rest, CRLDbInfo) + end + end; + {error, _Reason} -> + get_crls(Rest, CRLDbInfo) + end. + +cache_lookup(_, undefined) -> + []; +cache_lookup(URL, {{Cache, _}, _}) -> + #{path := Path} = uri_string:normalize(URL, [return_map]), + case ssl_pkix_db:lookup(string:trim(Path, leading, "/"), Cache) of + undefined -> + []; + [CRLs] -> + CRLs + end. + +handle_http(URI, Rest, {_, [{http, Timeout}]} = CRLDbInfo) -> + CRLs = http_lookup(URI, Rest, CRLDbInfo, Timeout), + %% Uncomment to improve performance, but need to + %% implement cache limit and or cleaning to prevent + %% DoS attack possibilities + %%insert(URI, {der, CRLs}), + CRLs; +handle_http(_, Rest, CRLDbInfo) -> + get_crls(Rest, CRLDbInfo). diff --git a/apps/emqx/test/emqx_api_lib_SUITE.erl b/apps/emqx/test/emqx_api_lib_SUITE.erl new file mode 100644 index 000000000..29f5c6095 --- /dev/null +++ b/apps/emqx/test/emqx_api_lib_SUITE.erl @@ -0,0 +1,101 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_api_lib_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_api_lib.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(DUMMY, dummy_module). + +all() -> emqx_common_test_helpers:all(?MODULE). + +init_per_suite(Config) -> + emqx_common_test_helpers:boot_modules(all), + emqx_common_test_helpers:start_apps([]), + Config. + +end_per_suite(_Config) -> + emqx_common_test_helpers:stop_apps([]). + +init_per_testcase(_Case, Config) -> + meck:new(?DUMMY, [non_strict]), + meck:expect(?DUMMY, expect_not_called, 1, fun(Node) -> throw({blow_this_up, Node}) end), + meck:expect(?DUMMY, expect_success, 1, {ok, success}), + meck:expect(?DUMMY, expect_error, 1, {error, error}), + Config. + +end_per_testcase(_Case, _Config) -> + meck:unload(?DUMMY). + +t_with_node(_) -> + test_with(fun emqx_api_lib:with_node/2, [<<"all">>]). + +t_with_node_or_cluster(_) -> + test_with(fun emqx_api_lib:with_node_or_cluster/2, []), + meck:reset(?DUMMY), + ?assertEqual( + ?OK(success), + emqx_api_lib:with_node_or_cluster( + <<"all">>, + fun ?DUMMY:expect_success/1 + ) + ), + ?assertMatch([{_, {?DUMMY, expect_success, [all]}, {ok, success}}], meck:history(?DUMMY)). + +%% helpers +test_with(TestFun, ExtraBadNodes) -> + % make sure this is an atom + 'unknownnode@unknownnohost', + BadNodes = + [ + <<"undefined">>, + <<"this_should_not_be_an_atom">>, + <<"unknownnode@unknownnohost">> + ] ++ ExtraBadNodes, + [ensure_not_found(TestFun(N, fun ?DUMMY:expect_not_called/1)) || N <- BadNodes], + ensure_not_called(?DUMMY, expect_not_called), + ensure_not_existing_atom(<<"this_should_not_be_an_atom">>), + + GoodNode = node(), + + ?assertEqual( + ?OK(success), + TestFun(GoodNode, fun ?DUMMY:expect_success/1) + ), + + ?assertEqual( + ?BAD_REQUEST(error), + TestFun(GoodNode, fun ?DUMMY:expect_error/1) + ), + ok. + +ensure_not_found(Result) -> + ?assertMatch({404, _}, Result). + +ensure_not_called(Mod, Fun) -> + ?assert(not meck:called(Mod, Fun, '_')). + +ensure_not_existing_atom(Bin) -> + try binary_to_existing_atom(Bin) of + _ -> throw(is_atom) + catch + error:badarg -> + ok + end. diff --git a/apps/emqx/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl index 2f433c73d..82d4038da 100644 --- a/apps/emqx/test/emqx_client_SUITE.erl +++ b/apps/emqx/test/emqx_client_SUITE.erl @@ -390,4 +390,10 @@ tls_certcn_as_clientid(TLSVsn, RequiredTLSVsn) -> {ok, _} = emqtt:connect(Client), #{clientinfo := #{clientid := CN}} = emqx_cm:get_chan_info(CN), confirm_tls_version(Client, RequiredTLSVsn), + %% verify that the peercert won't be stored in the conninfo + [ChannPid] = emqx_cm:lookup_channels(CN), + SysState = sys:get_state(ChannPid), + ChannelRecord = lists:keyfind(channel, 1, tuple_to_list(SysState)), + ConnInfo = lists:nth(2, tuple_to_list(ChannelRecord)), + ?assertMatch(#{peercert := undefined}, ConnInfo), emqtt:disconnect(Client). diff --git a/apps/emqx/test/emqx_common_test_helpers.erl b/apps/emqx/test/emqx_common_test_helpers.erl index 38f30b8c5..9a4461fac 100644 --- a/apps/emqx/test/emqx_common_test_helpers.erl +++ b/apps/emqx/test/emqx_common_test_helpers.erl @@ -16,7 +16,7 @@ -module(emqx_common_test_helpers). --include("emqx_authentication.hrl"). +-include_lib("emqx/include/emqx_authentication.hrl"). -type special_config_handler() :: fun(). @@ -85,6 +85,13 @@ reset_proxy/2 ]). +%% TLS certs API +-export([ + gen_ca/2, + gen_host_cert/3, + gen_host_cert/4 +]). + -define(CERTS_PATH(CertName), filename:join(["etc", "certs", CertName])). -define(MQTT_SSL_CLIENT_CERTS, [ @@ -202,7 +209,6 @@ start_apps(Apps, SpecAppConfig, Opts) when is_function(SpecAppConfig) -> %% Because, minirest, ekka etc.. application will scan these modules lists:foreach(fun load/1, [emqx | Apps]), ok = start_ekka(), - mnesia:clear_table(emqx_admin), ok = emqx_ratelimiter_SUITE:load_conf(), lists:foreach(fun(App) -> start_app(App, SpecAppConfig, Opts) end, [emqx | Apps]). @@ -262,12 +268,13 @@ app_schema(App) -> end. mustache_vars(App, Opts) -> - ExtraMustacheVars = maps:get(extra_mustache_vars, Opts, []), - [ - {platform_data_dir, app_path(App, "data")}, - {platform_etc_dir, app_path(App, "etc")}, - {platform_log_dir, app_path(App, "log")} - ] ++ ExtraMustacheVars. + ExtraMustacheVars = maps:get(extra_mustache_vars, Opts, #{}), + Defaults = #{ + platform_data_dir => app_path(App, "data"), + platform_etc_dir => app_path(App, "etc"), + platform_log_dir => app_path(App, "log") + }, + maps:merge(Defaults, ExtraMustacheVars). render_config_file(ConfigFile, Vars0) -> Temp = @@ -275,7 +282,7 @@ render_config_file(ConfigFile, Vars0) -> {ok, T} -> T; {error, Reason} -> error({failed_to_read_config_template, ConfigFile, Reason}) end, - Vars = [{atom_to_list(N), iolist_to_binary(V)} || {N, V} <- Vars0], + Vars = [{atom_to_list(N), iolist_to_binary(V)} || {N, V} <- maps:to_list(Vars0)], Targ = bbmustache:render(Temp, Vars), NewName = ConfigFile ++ ".rendered", ok = file:write_file(NewName, Targ), @@ -299,6 +306,7 @@ generate_config(SchemaModule, ConfigFile) when is_atom(SchemaModule) -> -spec stop_apps(list()) -> ok. stop_apps(Apps) -> [application:stop(App) || App <- Apps ++ [emqx, ekka, mria, mnesia]], + ok = mria_mnesia:delete_schema(), %% to avoid inter-suite flakiness application:unset_env(emqx, init_config_load_done), persistent_term:erase(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY), @@ -561,6 +569,7 @@ ensure_quic_listener(Name, UdpPort, ExtraSettings) -> mountpoint => <<>>, zone => default }, + Conf2 = maps:merge(Conf, ExtraSettings), emqx_config:put([listeners, quic, Name], Conf2), case emqx_listeners:start_listener(emqx_listeners:listener_id(quic, Name)) of @@ -723,7 +732,7 @@ setup_node(Node, Opts) when is_map(Opts) -> ConfigureGenRpc = maps:get(configure_gen_rpc, Opts, true), LoadSchema = maps:get(load_schema, Opts, true), SchemaMod = maps:get(schema_mod, Opts, emqx_schema), - LoadApps = maps:get(load_apps, Opts, [gen_rpc, emqx, ekka, mria] ++ Apps), + LoadApps = maps:get(load_apps, Opts, Apps), Env = maps:get(env, Opts, []), Conf = maps:get(conf, Opts, []), ListenerPorts = maps:get(listener_ports, Opts, [ @@ -741,12 +750,13 @@ setup_node(Node, Opts) when is_map(Opts) -> StartAutocluster = maps:get(start_autocluster, Opts, false), %% Load env before doing anything to avoid overriding - lists:foreach(fun(App) -> rpc:call(Node, ?MODULE, load, [App]) end, LoadApps), + [ok = erpc:call(Node, ?MODULE, load, [App]) || App <- [gen_rpc, ekka, mria, emqx | LoadApps]], + %% Ensure a clean mnesia directory for each run to avoid %% inter-test flakiness. MnesiaDataDir = filename:join([ PrivDataDir, - node(), + Node, integer_to_list(erlang:unique_integer()), "mnesia" ]), @@ -1073,6 +1083,104 @@ latency_up_proxy(off, Name, ProxyHost, ProxyPort) -> ). %%------------------------------------------------------------------------------- +%% TLS certs +%%------------------------------------------------------------------------------- +gen_ca(Path, Name) -> + %% Generate ca.pem and ca.key which will be used to generate certs + %% for hosts server and clients + ECKeyFile = filename(Path, "~s-ec.key", [Name]), + filelib:ensure_dir(ECKeyFile), + os:cmd("openssl ecparam -name secp256r1 > " ++ ECKeyFile), + Cmd = lists:flatten( + io_lib:format( + "openssl req -new -x509 -nodes " + "-newkey ec:~s " + "-keyout ~s -out ~s -days 3650 " + "-subj \"/C=SE/O=Internet Widgits Pty Ltd CA\"", + [ + ECKeyFile, + ca_key_name(Path, Name), + ca_cert_name(Path, Name) + ] + ) + ), + os:cmd(Cmd). + +ca_cert_name(Path, Name) -> + filename(Path, "~s.pem", [Name]). +ca_key_name(Path, Name) -> + filename(Path, "~s.key", [Name]). + +gen_host_cert(H, CaName, Path) -> + gen_host_cert(H, CaName, Path, #{}). + +gen_host_cert(H, CaName, Path, Opts) -> + ECKeyFile = filename(Path, "~s-ec.key", [CaName]), + CN = str(H), + HKey = filename(Path, "~s.key", [H]), + HCSR = filename(Path, "~s.csr", [H]), + HPEM = filename(Path, "~s.pem", [H]), + HEXT = filename(Path, "~s.extfile", [H]), + PasswordArg = + case maps:get(password, Opts, undefined) of + undefined -> + " -nodes "; + Password -> + io_lib:format(" -passout pass:'~s' ", [Password]) + end, + CSR_Cmd = + lists:flatten( + io_lib:format( + "openssl req -new ~s -newkey ec:~s " + "-keyout ~s -out ~s " + "-addext \"subjectAltName=DNS:~s\" " + "-addext keyUsage=digitalSignature,keyAgreement " + "-subj \"/C=SE/O=Internet Widgits Pty Ltd/CN=~s\"", + [PasswordArg, ECKeyFile, HKey, HCSR, CN, CN] + ) + ), + create_file( + HEXT, + "keyUsage=digitalSignature,keyAgreement\n" + "subjectAltName=DNS:~s\n", + [CN] + ), + CERT_Cmd = + lists:flatten( + io_lib:format( + "openssl x509 -req " + "-extfile ~s " + "-in ~s -CA ~s -CAkey ~s -CAcreateserial " + "-out ~s -days 500", + [ + HEXT, + HCSR, + ca_cert_name(Path, CaName), + ca_key_name(Path, CaName), + HPEM + ] + ) + ), + ct:pal(os:cmd(CSR_Cmd)), + ct:pal(os:cmd(CERT_Cmd)), + file:delete(HEXT). + +filename(Path, F, A) -> + filename:join(Path, str(io_lib:format(F, A))). + +str(Arg) -> + binary_to_list(iolist_to_binary(Arg)). + +create_file(Filename, Fmt, Args) -> + filelib:ensure_dir(Filename), + {ok, F} = file:open(Filename, [write]), + try + io:format(F, Fmt, Args) + after + file:close(F) + end, + ok. +%%------------------------------------------------------------------------------- %% Testcase teardown utilities %%------------------------------------------------------------------------------- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE.erl b/apps/emqx/test/emqx_crl_cache_SUITE.erl new file mode 100644 index 000000000..01f9c7172 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE.erl @@ -0,0 +1,1070 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_crl_cache_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +%% from ssl_manager.erl +-record(state, { + session_cache_client, + session_cache_client_cb, + session_lifetime, + certificate_db, + session_validation_timer, + session_cache_client_max, + session_client_invalidator, + options, + client_session_order +}). + +-define(DEFAULT_URL, "http://localhost:9878/intermediate.crl.pem"). + +%%-------------------------------------------------------------------- +%% CT boilerplate +%%-------------------------------------------------------------------- + +all() -> + emqx_common_test_helpers:all(?MODULE). + +init_per_suite(Config) -> + application:load(emqx), + emqx_config:save_schema_mod_and_names(emqx_schema), + emqx_common_test_helpers:boot_modules(all), + Config. + +end_per_suite(_Config) -> + ok. + +init_per_testcase(TestCase, Config) when + TestCase =:= t_cache; + TestCase =:= t_filled_cache; + TestCase =:= t_revoked +-> + ct:timetrap({seconds, 30}), + DataDir = ?config(data_dir, Config), + CRLFile = filename:join([DataDir, "intermediate-revoked.crl.pem"]), + {ok, CRLPem} = file:read_file(CRLFile), + [{'CertificateList', CRLDer, not_encrypted}] = public_key:pem_decode(CRLPem), + ok = snabbkaffe:start_trace(), + ServerPid = start_crl_server(CRLPem), + IsCached = lists:member(TestCase, [t_filled_cache, t_revoked]), + ok = setup_crl_options(Config, #{is_cached => IsCached}), + [ + {crl_pem, CRLPem}, + {crl_der, CRLDer}, + {http_server, ServerPid} + | Config + ]; +init_per_testcase(t_revoke_then_refresh, Config) -> + ct:timetrap({seconds, 120}), + DataDir = ?config(data_dir, Config), + CRLFileNotRevoked = filename:join([DataDir, "intermediate-not-revoked.crl.pem"]), + {ok, CRLPemNotRevoked} = file:read_file(CRLFileNotRevoked), + [{'CertificateList', CRLDerNotRevoked, not_encrypted}] = public_key:pem_decode( + CRLPemNotRevoked + ), + CRLFileRevoked = filename:join([DataDir, "intermediate-revoked.crl.pem"]), + {ok, CRLPemRevoked} = file:read_file(CRLFileRevoked), + [{'CertificateList', CRLDerRevoked, not_encrypted}] = public_key:pem_decode(CRLPemRevoked), + ok = snabbkaffe:start_trace(), + ServerPid = start_crl_server(CRLPemNotRevoked), + ExtraVars = #{refresh_interval => <<"10s">>}, + ok = setup_crl_options(Config, #{is_cached => true, extra_vars => ExtraVars}), + [ + {crl_pem_not_revoked, CRLPemNotRevoked}, + {crl_der_not_revoked, CRLDerNotRevoked}, + {crl_pem_revoked, CRLPemRevoked}, + {crl_der_revoked, CRLDerRevoked}, + {http_server, ServerPid} + | Config + ]; +init_per_testcase(t_cache_overflow, Config) -> + ct:timetrap({seconds, 120}), + DataDir = ?config(data_dir, Config), + CRLFileRevoked = filename:join([DataDir, "intermediate-revoked.crl.pem"]), + {ok, CRLPemRevoked} = file:read_file(CRLFileRevoked), + ok = snabbkaffe:start_trace(), + ServerPid = start_crl_server(CRLPemRevoked), + ExtraVars = #{cache_capacity => <<"2">>}, + ok = setup_crl_options(Config, #{is_cached => false, extra_vars => ExtraVars}), + [ + {http_server, ServerPid} + | Config + ]; +init_per_testcase(t_not_cached_and_unreachable, Config) -> + ct:timetrap({seconds, 30}), + DataDir = ?config(data_dir, Config), + CRLFile = filename:join([DataDir, "intermediate-revoked.crl.pem"]), + {ok, CRLPem} = file:read_file(CRLFile), + [{'CertificateList', CRLDer, not_encrypted}] = public_key:pem_decode(CRLPem), + ok = snabbkaffe:start_trace(), + application:stop(cowboy), + ok = setup_crl_options(Config, #{is_cached => false}), + [ + {crl_pem, CRLPem}, + {crl_der, CRLDer} + | Config + ]; +init_per_testcase(t_refresh_config, Config) -> + ct:timetrap({seconds, 30}), + DataDir = ?config(data_dir, Config), + CRLFile = filename:join([DataDir, "intermediate-revoked.crl.pem"]), + {ok, CRLPem} = file:read_file(CRLFile), + [{'CertificateList', CRLDer, not_encrypted}] = public_key:pem_decode(CRLPem), + TestPid = self(), + ok = meck:new(emqx_crl_cache, [non_strict, passthrough, no_history, no_link]), + meck:expect( + emqx_crl_cache, + http_get, + fun(URL, _HTTPTimeout) -> + ct:pal("http get crl ~p", [URL]), + TestPid ! {http_get, URL}, + {ok, {{"HTTP/1.0", 200, "OK"}, [], CRLPem}} + end + ), + ok = snabbkaffe:start_trace(), + ok = setup_crl_options(Config, #{is_cached => false}), + [ + {crl_pem, CRLPem}, + {crl_der, CRLDer} + | Config + ]; +init_per_testcase(TestCase, Config) when + TestCase =:= t_update_listener; + TestCase =:= t_validations +-> + %% when running emqx standalone tests, we can't use those + %% features. + case does_module_exist(emqx_mgmt_api_test_util) of + true -> + ct:timetrap({seconds, 30}), + DataDir = ?config(data_dir, Config), + PrivDir = ?config(priv_dir, Config), + CRLFile = filename:join([DataDir, "intermediate-revoked.crl.pem"]), + {ok, CRLPem} = file:read_file(CRLFile), + ok = snabbkaffe:start_trace(), + ServerPid = start_crl_server(CRLPem), + ConfFilePath = filename:join([DataDir, "emqx_just_verify.conf"]), + emqx_mgmt_api_test_util:init_suite( + [emqx_conf], + fun emqx_mgmt_api_test_util:set_special_configs/1, + #{ + extra_mustache_vars => #{ + test_data_dir => DataDir, + test_priv_dir => PrivDir + }, + conf_file_path => ConfFilePath + } + ), + [ + {http_server, ServerPid} + | Config + ]; + false -> + [{skip_does_not_apply, true} | Config] + end; +init_per_testcase(_TestCase, Config) -> + ct:timetrap({seconds, 30}), + DataDir = ?config(data_dir, Config), + CRLFile = filename:join([DataDir, "intermediate-revoked.crl.pem"]), + {ok, CRLPem} = file:read_file(CRLFile), + [{'CertificateList', CRLDer, not_encrypted}] = public_key:pem_decode(CRLPem), + TestPid = self(), + ok = meck:new(emqx_crl_cache, [non_strict, passthrough, no_history, no_link]), + meck:expect( + emqx_crl_cache, + http_get, + fun(URL, _HTTPTimeout) -> + ct:pal("http get crl ~p", [URL]), + TestPid ! {http_get, URL}, + {ok, {{"HTTP/1.0", 200, 'OK'}, [], CRLPem}} + end + ), + ok = snabbkaffe:start_trace(), + [ + {crl_pem, CRLPem}, + {crl_der, CRLDer} + | Config + ]. + +end_per_testcase(TestCase, Config) when + TestCase =:= t_cache; + TestCase =:= t_filled_cache; + TestCase =:= t_revoked +-> + ServerPid = ?config(http_server, Config), + emqx_crl_cache_http_server:stop(ServerPid), + emqx_common_test_helpers:stop_apps([]), + clear_listeners(), + application:stop(cowboy), + clear_crl_cache(), + ok = snabbkaffe:stop(), + ok; +end_per_testcase(TestCase, Config) when + TestCase =:= t_revoke_then_refresh; + TestCase =:= t_cache_overflow +-> + ServerPid = ?config(http_server, Config), + emqx_crl_cache_http_server:stop(ServerPid), + emqx_common_test_helpers:stop_apps([]), + clear_listeners(), + clear_crl_cache(), + application:stop(cowboy), + ok = snabbkaffe:stop(), + ok; +end_per_testcase(t_not_cached_and_unreachable, _Config) -> + emqx_common_test_helpers:stop_apps([]), + clear_listeners(), + clear_crl_cache(), + ok = snabbkaffe:stop(), + ok; +end_per_testcase(t_refresh_config, _Config) -> + meck:unload([emqx_crl_cache]), + clear_crl_cache(), + emqx_common_test_helpers:stop_apps([]), + clear_listeners(), + clear_crl_cache(), + application:stop(cowboy), + ok = snabbkaffe:stop(), + ok; +end_per_testcase(TestCase, Config) when + TestCase =:= t_update_listener; + TestCase =:= t_validations +-> + Skip = proplists:get_bool(skip_does_not_apply, Config), + case Skip of + true -> + ok; + false -> + ServerPid = ?config(http_server, Config), + emqx_crl_cache_http_server:stop(ServerPid), + emqx_mgmt_api_test_util:end_suite([emqx_conf]), + clear_listeners(), + ok = snabbkaffe:stop(), + clear_crl_cache(), + ok + end; +end_per_testcase(_TestCase, _Config) -> + meck:unload([emqx_crl_cache]), + clear_crl_cache(), + ok = snabbkaffe:stop(), + ok. + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +does_module_exist(Mod) -> + case erlang:module_loaded(Mod) of + true -> + true; + false -> + case code:ensure_loaded(Mod) of + ok -> + true; + {module, Mod} -> + true; + _ -> + false + end + end. + +clear_listeners() -> + emqx_config:put([listeners], #{}), + emqx_config:put_raw([listeners], #{}), + ok. + +assert_http_get(URL) -> + receive + {http_get, URL} -> + ok + after 1000 -> + ct:pal("mailbox: ~p", [process_info(self(), messages)]), + error({should_have_requested, URL}) + end. + +get_crl_cache_table() -> + #state{certificate_db = [_, _, _, {Ref, _}]} = sys:get_state(ssl_manager), + Ref. + +start_crl_server(Port, CRLPem) -> + {ok, LSock} = gen_tcp:listen(Port, [binary, {active, true}, reusedaddr]), + spawn_link(fun() -> accept_loop(LSock, CRLPem) end), + ok. + +accept_loop(LSock, CRLPem) -> + case gen_tcp:accept(LSock) of + {ok, Sock} -> + Worker = spawn_link(fun() -> crl_loop(Sock, CRLPem) end), + gen_tcp:controlling_process(Sock, Worker), + accept_loop(LSock, CRLPem); + {error, Reason} -> + error({accept_error, Reason}) + end. + +crl_loop(Sock, CRLPem) -> + receive + {tcp, Sock, _Data} -> + gen_tcp:send(Sock, CRLPem), + crl_loop(Sock, CRLPem); + _Msg -> + ok + end. + +drain_msgs() -> + receive + _Msg -> + drain_msgs() + after 0 -> + ok + end. + +clear_crl_cache() -> + %% reset the CRL cache + exit(whereis(ssl_manager), kill), + ok. + +force_cacertfile(Cacertfile) -> + {SSLListeners0, OtherListeners} = lists:partition( + fun(#{proto := Proto}) -> Proto =:= ssl end, + emqx:get_env(listeners) + ), + SSLListeners = + lists:map( + fun(Listener = #{opts := Opts0}) -> + SSLOpts0 = proplists:get_value(ssl_options, Opts0), + %% it injects some garbage... + SSLOpts1 = lists:keydelete(cacertfile, 1, lists:keydelete(cacertfile, 1, SSLOpts0)), + SSLOpts2 = [{cacertfile, Cacertfile} | SSLOpts1], + Opts1 = lists:keyreplace(ssl_options, 1, Opts0, {ssl_options, SSLOpts2}), + Listener#{opts => Opts1} + end, + SSLListeners0 + ), + application:set_env(emqx, listeners, SSLListeners ++ OtherListeners), + ok. + +setup_crl_options(Config, #{is_cached := IsCached} = Opts) -> + DataDir = ?config(data_dir, Config), + ConfFilePath = filename:join([DataDir, "emqx.conf"]), + Defaults = #{ + refresh_interval => <<"11m">>, + cache_capacity => <<"100">>, + test_data_dir => DataDir + }, + ExtraVars0 = maps:get(extra_vars, Opts, #{}), + ExtraVars = maps:merge(Defaults, ExtraVars0), + emqx_common_test_helpers:start_apps( + [], + fun(_) -> ok end, + #{ + extra_mustache_vars => ExtraVars, + conf_file_path => ConfFilePath + } + ), + case IsCached of + true -> + %% wait the cache to be filled + emqx_crl_cache:refresh(?DEFAULT_URL), + receive + {http_get, <>} -> ok + after 1_000 -> + ct:pal("mailbox: ~p", [process_info(self(), messages)]), + error(crl_cache_not_filled) + end; + false -> + %% ensure cache is empty + clear_crl_cache(), + ct:sleep(200), + ok + end, + drain_msgs(), + ok. + +start_crl_server(CRLPem) -> + application:ensure_all_started(cowboy), + {ok, ServerPid} = emqx_crl_cache_http_server:start_link(self(), 9878, CRLPem, []), + receive + {ServerPid, ready} -> ok + after 1000 -> error(timeout_starting_http_server) + end, + ServerPid. + +request(Method, Url, QueryParams, Body) -> + AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + Opts = #{return_all => true}, + case emqx_mgmt_api_test_util:request_api(Method, Url, QueryParams, AuthHeader, Body, Opts) of + {ok, {Reason, Headers, BodyR}} -> + {ok, {Reason, Headers, emqx_json:decode(BodyR, [return_maps])}}; + Error -> + Error + end. + +get_listener_via_api(ListenerId) -> + Path = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]), + request(get, Path, [], []). + +update_listener_via_api(ListenerId, NewConfig) -> + Path = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]), + request(put, Path, [], NewConfig). + +assert_successful_connection(Config) -> + assert_successful_connection(Config, default). + +assert_successful_connection(Config, ClientNum) -> + DataDir = ?config(data_dir, Config), + Num = + case ClientNum of + default -> ""; + _ -> integer_to_list(ClientNum) + end, + ClientCert = filename:join(DataDir, "client" ++ Num ++ ".cert.pem"), + ClientKey = filename:join(DataDir, "client" ++ Num ++ ".key.pem"), + %% 1) At first, the cache is empty, and the CRL is fetched and + %% cached on the fly. + {ok, C0} = emqtt:start_link([ + {ssl, true}, + {ssl_opts, [ + {certfile, ClientCert}, + {keyfile, ClientKey} + ]}, + {port, 8883} + ]), + ?tp_span( + mqtt_client_connection, + #{client_num => ClientNum}, + begin + {ok, _} = emqtt:connect(C0), + emqtt:stop(C0), + ok + end + ). + +trace_between(Trace0, Marker1, Marker2) -> + {Trace1, [_ | _]} = ?split_trace_at(#{?snk_kind := Marker2}, Trace0), + {[_ | _], [_ | Trace2]} = ?split_trace_at(#{?snk_kind := Marker1}, Trace1), + Trace2. + +of_kinds(Trace0, Kinds0) -> + Kinds = sets:from_list(Kinds0, [{version, 2}]), + lists:filter( + fun(#{?snk_kind := K}) -> sets:is_element(K, Kinds) end, + Trace0 + ). + +%%-------------------------------------------------------------------- +%% Test cases +%%-------------------------------------------------------------------- + +t_init_empty_urls(_Config) -> + Ref = get_crl_cache_table(), + ?assertEqual([], ets:tab2list(Ref)), + ?assertMatch({ok, _}, emqx_crl_cache:start_link()), + receive + {http_get, _} -> + error(should_not_make_http_request) + after 1000 -> ok + end, + ?assertEqual([], ets:tab2list(Ref)), + ok. + +t_manual_refresh(Config) -> + CRLDer = ?config(crl_der, Config), + Ref = get_crl_cache_table(), + ?assertEqual([], ets:tab2list(Ref)), + {ok, _} = emqx_crl_cache:start_link(), + URL = "http://localhost/crl.pem", + ok = snabbkaffe:start_trace(), + ?wait_async_action( + ?assertEqual(ok, emqx_crl_cache:refresh(URL)), + #{?snk_kind := crl_cache_insert}, + 5_000 + ), + ok = snabbkaffe:stop(), + ?assertEqual( + [{"crl.pem", [CRLDer]}], + ets:tab2list(Ref) + ), + ok. + +t_refresh_request_error(_Config) -> + meck:expect( + emqx_crl_cache, + http_get, + fun(_URL, _HTTPTimeout) -> + {ok, {{"HTTP/1.0", 404, 'Not Found'}, [], <<"not found">>}} + end + ), + {ok, _} = emqx_crl_cache:start_link(), + URL = "http://localhost/crl.pem", + ?check_trace( + ?wait_async_action( + ?assertEqual(ok, emqx_crl_cache:refresh(URL)), + #{?snk_kind := crl_cache_insert}, + 5_000 + ), + fun(Trace) -> + ?assertMatch( + [#{error := {bad_response, #{code := 404}}}], + ?of_kind(crl_refresh_failure, Trace) + ), + ok + end + ), + ok = snabbkaffe:stop(), + ok. + +t_refresh_invalid_response(_Config) -> + meck:expect( + emqx_crl_cache, + http_get, + fun(_URL, _HTTPTimeout) -> + {ok, {{"HTTP/1.0", 200, 'OK'}, [], <<"not a crl">>}} + end + ), + {ok, _} = emqx_crl_cache:start_link(), + URL = "http://localhost/crl.pem", + ?check_trace( + ?wait_async_action( + ?assertEqual(ok, emqx_crl_cache:refresh(URL)), + #{?snk_kind := crl_cache_insert}, + 5_000 + ), + fun(Trace) -> + ?assertMatch( + [#{crls := []}], + ?of_kind(crl_cache_insert, Trace) + ), + ok + end + ), + ok = snabbkaffe:stop(), + ok. + +t_refresh_http_error(_Config) -> + meck:expect( + emqx_crl_cache, + http_get, + fun(_URL, _HTTPTimeout) -> + {error, timeout} + end + ), + {ok, _} = emqx_crl_cache:start_link(), + URL = "http://localhost/crl.pem", + ?check_trace( + ?wait_async_action( + ?assertEqual(ok, emqx_crl_cache:refresh(URL)), + #{?snk_kind := crl_cache_insert}, + 5_000 + ), + fun(Trace) -> + ?assertMatch( + [#{error := {http_error, timeout}}], + ?of_kind(crl_refresh_failure, Trace) + ), + ok + end + ), + ok = snabbkaffe:stop(), + ok. + +t_unknown_messages(_Config) -> + {ok, Server} = emqx_crl_cache:start_link(), + gen_server:call(Server, foo), + gen_server:cast(Server, foo), + Server ! foo, + ok. + +t_evict(_Config) -> + {ok, _} = emqx_crl_cache:start_link(), + URL = "http://localhost/crl.pem", + ?wait_async_action( + ?assertEqual(ok, emqx_crl_cache:refresh(URL)), + #{?snk_kind := crl_cache_insert}, + 5_000 + ), + Ref = get_crl_cache_table(), + ?assertMatch([{"crl.pem", _}], ets:tab2list(Ref)), + {ok, {ok, _}} = ?wait_async_action( + emqx_crl_cache:evict(URL), + #{?snk_kind := crl_cache_evict} + ), + ?assertEqual([], ets:tab2list(Ref)), + ok. + +t_cache(Config) -> + DataDir = ?config(data_dir, Config), + ClientCert = filename:join(DataDir, "client.cert.pem"), + ClientKey = filename:join(DataDir, "client.key.pem"), + %% 1) At first, the cache is empty, and the CRL is fetched and + %% cached on the fly. + {ok, C0} = emqtt:start_link([ + {ssl, true}, + {ssl_opts, [ + {certfile, ClientCert}, + {keyfile, ClientKey} + ]}, + {port, 8883} + ]), + {ok, _} = emqtt:connect(C0), + receive + {http_get, _} -> ok + after 500 -> + emqtt:stop(C0), + error(should_have_checked_server) + end, + emqtt:stop(C0), + %% 2) When another client using the cached CRL URL connects later, + %% it uses the cache. + {ok, C1} = emqtt:start_link([ + {ssl, true}, + {ssl_opts, [ + {certfile, ClientCert}, + {keyfile, ClientKey} + ]}, + {port, 8883} + ]), + {ok, _} = emqtt:connect(C1), + receive + {http_get, _} -> + emqtt:stop(C1), + error(should_not_have_checked_server) + after 500 -> ok + end, + emqtt:stop(C1), + + ok. + +t_cache_overflow(Config) -> + %% we have capacity = 2 here. + ?check_trace( + begin + %% First and second connections goes into the cache + ?tp(first_connections, #{}), + assert_successful_connection(Config, 1), + assert_successful_connection(Config, 2), + %% These should be cached + ?tp(first_reconnections, #{}), + assert_successful_connection(Config, 1), + assert_successful_connection(Config, 2), + %% A third client connects and evicts the oldest URL (1) + ?tp(first_eviction, #{}), + assert_successful_connection(Config, 3), + assert_successful_connection(Config, 3), + %% URL (1) connects again and needs to be re-cached; this + %% time, (2) gets evicted + ?tp(second_eviction, #{}), + assert_successful_connection(Config, 1), + %% TODO: force race condition where the same URL is fetched + %% at the same time and tries to be registered + ?tp(test_end, #{}), + ok + end, + fun(Trace) -> + URL1 = "http://localhost:9878/intermediate1.crl.pem", + URL2 = "http://localhost:9878/intermediate2.crl.pem", + URL3 = "http://localhost:9878/intermediate3.crl.pem", + Kinds = [ + mqtt_client_connection, + new_crl_url_inserted, + crl_cache_ensure_timer, + crl_cache_overflow + ], + Trace1 = of_kinds( + trace_between(Trace, first_connections, first_reconnections), + Kinds + ), + ?assertMatch( + [ + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := start, + client_num := 1 + }, + #{ + ?snk_kind := new_crl_url_inserted, + url := URL1 + }, + #{ + ?snk_kind := crl_cache_ensure_timer, + url := URL1 + }, + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := {complete, ok}, + client_num := 1 + }, + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := start, + client_num := 2 + }, + #{ + ?snk_kind := new_crl_url_inserted, + url := URL2 + }, + #{ + ?snk_kind := crl_cache_ensure_timer, + url := URL2 + }, + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := {complete, ok}, + client_num := 2 + } + ], + Trace1 + ), + Trace2 = of_kinds( + trace_between(Trace, first_reconnections, first_eviction), + Kinds + ), + ?assertMatch( + [ + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := start, + client_num := 1 + }, + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := {complete, ok}, + client_num := 1 + }, + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := start, + client_num := 2 + }, + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := {complete, ok}, + client_num := 2 + } + ], + Trace2 + ), + Trace3 = of_kinds( + trace_between(Trace, first_eviction, second_eviction), + Kinds + ), + ?assertMatch( + [ + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := start, + client_num := 3 + }, + #{ + ?snk_kind := new_crl_url_inserted, + url := URL3 + }, + #{ + ?snk_kind := crl_cache_overflow, + oldest_url := URL1 + }, + #{ + ?snk_kind := crl_cache_ensure_timer, + url := URL3 + }, + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := {complete, ok}, + client_num := 3 + }, + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := start, + client_num := 3 + }, + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := {complete, ok}, + client_num := 3 + } + ], + Trace3 + ), + Trace4 = of_kinds( + trace_between(Trace, second_eviction, test_end), + Kinds + ), + ?assertMatch( + [ + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := start, + client_num := 1 + }, + #{ + ?snk_kind := new_crl_url_inserted, + url := URL1 + }, + #{ + ?snk_kind := crl_cache_overflow, + oldest_url := URL2 + }, + #{ + ?snk_kind := crl_cache_ensure_timer, + url := URL1 + }, + #{ + ?snk_kind := mqtt_client_connection, + ?snk_span := {complete, ok}, + client_num := 1 + } + ], + Trace4 + ), + ok + end + ). + +%% check that the URL in the certificate is *not* checked if the cache +%% contains that URL. +t_filled_cache(Config) -> + DataDir = ?config(data_dir, Config), + ClientCert = filename:join(DataDir, "client.cert.pem"), + ClientKey = filename:join(DataDir, "client.key.pem"), + {ok, C} = emqtt:start_link([ + {ssl, true}, + {ssl_opts, [ + {certfile, ClientCert}, + {keyfile, ClientKey} + ]}, + {port, 8883} + ]), + {ok, _} = emqtt:connect(C), + receive + http_get -> + emqtt:stop(C), + error(should_have_used_cache) + after 500 -> ok + end, + emqtt:stop(C), + ok. + +%% If the CRL is not cached when the client tries to connect and the +%% CRL server is unreachable, the client will be denied connection. +t_not_cached_and_unreachable(Config) -> + DataDir = ?config(data_dir, Config), + ClientCert = filename:join(DataDir, "client.cert.pem"), + ClientKey = filename:join(DataDir, "client.key.pem"), + {ok, C} = emqtt:start_link([ + {ssl, true}, + {ssl_opts, [ + {certfile, ClientCert}, + {keyfile, ClientKey} + ]}, + {port, 8883} + ]), + Ref = get_crl_cache_table(), + ?assertEqual([], ets:tab2list(Ref)), + process_flag(trap_exit, true), + ?assertMatch({error, {{shutdown, {tls_alert, {bad_certificate, _}}}, _}}, emqtt:connect(C)), + ok. + +t_revoked(Config) -> + DataDir = ?config(data_dir, Config), + ClientCert = filename:join(DataDir, "client-revoked.cert.pem"), + ClientKey = filename:join(DataDir, "client-revoked.key.pem"), + {ok, C} = emqtt:start_link([ + {ssl, true}, + {ssl_opts, [ + {certfile, ClientCert}, + {keyfile, ClientKey} + ]}, + {port, 8883} + ]), + process_flag(trap_exit, true), + Res = emqtt:connect(C), + %% apparently, sometimes there's some race condition in + %% `emqtt_sock:ssl_upgrade' when it calls + %% `ssl:conetrolling_process' and a bad match happens at that + %% point. + case Res of + {error, {{shutdown, {tls_alert, {certificate_revoked, _}}}, _}} -> + ok; + {error, closed} -> + %% race condition? + ok; + _ -> + ct:fail("unexpected result: ~p", [Res]) + end, + ok. + +t_revoke_then_refresh(Config) -> + DataDir = ?config(data_dir, Config), + CRLPemRevoked = ?config(crl_pem_revoked, Config), + ClientCert = filename:join(DataDir, "client-revoked.cert.pem"), + ClientKey = filename:join(DataDir, "client-revoked.key.pem"), + {ok, C0} = emqtt:start_link([ + {ssl, true}, + {ssl_opts, [ + {certfile, ClientCert}, + {keyfile, ClientKey} + ]}, + {port, 8883} + ]), + %% At first, the CRL contains no revoked entries, so the client + %% should be allowed connection. + ?assertMatch({ok, _}, emqtt:connect(C0)), + emqtt:stop(C0), + + %% Now we update the CRL on the server and wait for the cache to + %% be refreshed. + {true, {ok, _}} = + ?wait_async_action( + emqx_crl_cache_http_server:set_crl(CRLPemRevoked), + #{?snk_kind := crl_refresh_timer_done}, + 70_000 + ), + + %% The *same client* should now be denied connection. + {ok, C1} = emqtt:start_link([ + {ssl, true}, + {ssl_opts, [ + {certfile, ClientCert}, + {keyfile, ClientKey} + ]}, + {port, 8883} + ]), + process_flag(trap_exit, true), + ?assertMatch( + {error, {{shutdown, {tls_alert, {certificate_revoked, _}}}, _}}, emqtt:connect(C1) + ), + ok. + +%% check that we can start with a non-crl listener and restart it with +%% the new crl config. +t_update_listener(Config) -> + case proplists:get_bool(skip_does_not_apply, Config) of + true -> + ok; + false -> + do_t_update_listener(Config) + end. + +do_t_update_listener(Config) -> + DataDir = ?config(data_dir, Config), + Keyfile = filename:join([DataDir, "server.key.pem"]), + Certfile = filename:join([DataDir, "server.cert.pem"]), + Cacertfile = filename:join([DataDir, "ca-chain.cert.pem"]), + ClientCert = filename:join(DataDir, "client-revoked.cert.pem"), + ClientKey = filename:join(DataDir, "client-revoked.key.pem"), + + %% no crl at first + ListenerId = "ssl:default", + {ok, {{_, 200, _}, _, ListenerData0}} = get_listener_via_api(ListenerId), + ?assertMatch( + #{ + <<"ssl_options">> := + #{ + <<"enable_crl_check">> := false, + <<"verify">> := <<"verify_peer">> + } + }, + ListenerData0 + ), + {ok, C0} = emqtt:start_link([ + {ssl, true}, + {ssl_opts, [ + {certfile, ClientCert}, + {keyfile, ClientKey} + ]}, + {port, 8883} + ]), + %% At first, the CRL contains no revoked entries, so the client + %% should be allowed connection. + ?assertMatch({ok, _}, emqtt:connect(C0)), + emqtt:stop(C0), + + %% configure crl + CRLConfig = + #{ + <<"ssl_options">> => + #{ + <<"keyfile">> => Keyfile, + <<"certfile">> => Certfile, + <<"cacertfile">> => Cacertfile, + <<"enable_crl_check">> => true + } + }, + ListenerData1 = emqx_map_lib:deep_merge(ListenerData0, CRLConfig), + {ok, {_, _, ListenerData2}} = update_listener_via_api(ListenerId, ListenerData1), + ?assertMatch( + #{ + <<"ssl_options">> := + #{ + <<"enable_crl_check">> := true, + <<"verify">> := <<"verify_peer">> + } + }, + ListenerData2 + ), + + %% Now should use CRL information to block connection + process_flag(trap_exit, true), + {ok, C1} = emqtt:start_link([ + {ssl, true}, + {ssl_opts, [ + {certfile, ClientCert}, + {keyfile, ClientKey} + ]}, + {port, 8883} + ]), + ?assertMatch( + {error, {{shutdown, {tls_alert, {certificate_revoked, _}}}, _}}, emqtt:connect(C1) + ), + assert_http_get(<>), + + ok. + +t_validations(Config) -> + case proplists:get_bool(skip_does_not_apply, Config) of + true -> + ok; + false -> + do_t_validations(Config) + end. + +do_t_validations(_Config) -> + ListenerId = <<"ssl:default">>, + {ok, {{_, 200, _}, _, ListenerData0}} = get_listener_via_api(ListenerId), + + ListenerData1 = + emqx_map_lib:deep_merge( + ListenerData0, + #{ + <<"ssl_options">> => + #{ + <<"enable_crl_check">> => true, + <<"verify">> => <<"verify_none">> + } + } + ), + {error, {_, _, ResRaw1}} = update_listener_via_api(ListenerId, ListenerData1), + #{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw1} = + emqx_json:decode(ResRaw1, [return_maps]), + ?assertMatch( + #{ + <<"mismatches">> := + #{ + <<"listeners:ssl_not_required_bind">> := + #{ + <<"reason">> := + <<"verify must be verify_peer when CRL check is enabled">> + } + } + }, + emqx_json:decode(MsgRaw1, [return_maps]) + ), + + ok. diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/ca-chain.cert.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/ca-chain.cert.pem new file mode 100644 index 000000000..eaabd2445 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/ca-chain.cert.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +MIIF+zCCA+OgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQK +DAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJvb3RD +QTAeFw0yMzAxMTIxMzA4MTZaFw0zMzAxMDkxMzA4MTZaMGsxCzAJBgNVBAYTAlNF +MRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcGA1UE +CwwQTXlJbnRlcm1lZGlhdGVDQTEZMBcGA1UEAwwQTXlJbnRlcm1lZGlhdGVDQTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQG7dMeU/y9HDNHzhydR0bm +wN9UGplqJOJPwqJRaZZcrn9umgJ9SU2il2ceEVxMDwzBWCRKJO5/H9A9k13SqsXM +2c2c9xXfIF1kb820lCm1Uow5hZ/auDjxliNk9kNJDigCRi3QoIs/dVeWzFsgEC2l +gxRqauN2eNFb6/yXY788YALHBsCRV2NFOFXxtPsvLXpD9Q/8EqYsSMuLARRdHVNU +ryaEF5lhShpcuz0TlIuTy2TiuXJUtJ+p7a4Z7friZ6JsrmQWsVQBj44F8TJRHWzW +C7vm9c+dzEX9eqbr5iPL+L4ctMW9Lz6ePcYfIXne6CElusRUf8G+xM1uwovF9bpV ++9IqY7tAu9G1iY9iNtJgNNDKOCcOGKcZCx6Cg1XYOEKReNnUMazvYeqRrrjV5WQ0 +vOcD5zcBRNTXCddCLa7U0guXP9mQrfuk4NTH1Bt77JieTJ8cfDXHwtaKf6aGbmZP +wl1Xi/GuXNUP/xeog78RKyFwBmjt2JKwvWzMpfmH4mEkG9moh2alva+aEz6LIJuP +16g6s0Q6c793/OvUtpNcewHw4Vjn39LD9o6VLp854G4n8dVpUWSbWS+sXD1ZE69H +g/sMNMyq+09ufkbewY8xoCm/rQ1pqDZAVMWsstJEaYu7b/eb7R+RGOj1YECCV/Yp +EZPdDotbSNRkIi2d/a1NAgMBAAGjgaQwgaEwHQYDVR0OBBYEFExwhjsVUom6tQ+S +qq6xMUETvnPzMB8GA1UdIwQYMBaAFD90kfU5pc5l48THu0Ayj9SNpHuhMBIGA1Ud +EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDsGA1UdHwQ0MDIwMKAuoCyG +Kmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUuY3JsLnBlbTANBgkq +hkiG9w0BAQsFAAOCAgEAK6NgdWQYtPNKQNBGjsgtgqTRh+k30iqSO6Y3yE1KGABO +EuQdVqkC2qUIbCB0M0qoV0ab50KNLfU6cbshggW4LDpcMpoQpI05fukNh1jm3ZuZ +0xsB7vlmlsv00tpqmfIl/zykPDynHKOmFh/hJP/KetMy4+wDv4/+xP31UdEj5XvG +HvMtuqOS23A+H6WPU7ol7KzKBnU2zz/xekvPbUD3JqV+ynP5bgbIZHAndd0o9T8e +NFX23Us4cTenU2/ZlOq694bRzGaK+n3Ksz995Nbtzv5fbUgqmf7Mcq4iHGRVtV11 +MRyBrsXZp2vbF63c4hrf2Zd6SWRoaDKRhP2DMhajpH9zZASSTlfejg/ZRO2s+Clh +YrSTkeMAdnRt6i/q4QRcOTCfsX75RFM5v67njvTXsSaSTnAwaPi78tRtf+WSh0EP +VVPzy++BszBVlJ1VAf7soWZHCjZxZ8ZPqVTy5okoHwWQ09WmYe8GfulDh1oj0wbK +3FjN7bODWHJN+bFf5aQfK+tumYKoPG8RXL6QxpEzjFWjxhIMJHHMKfDWnAV1o1+7 +/1/aDzq7MzEYBbrgQR7oE5ZHtyqhCf9LUgw0Kr7/8QWuNAdeDCJzjXRROU0hJczp +dOyfRlLbHmLLmGOnROlx6LsGNQ17zuz6SPi7ei8/ylhykawDOAGkM1+xFakmQhM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUYjc7hD7/UJ0/VPADfNfp/WpOwRowDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJ +U3RvY2tob2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENB +MREwDwYDVQQDDAhNeVJvb3RDQTAeFw0yMzAxMTIxMzA4MTRaFw00MzAxMDcxMzA4 +MTRaMG8xCzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAcM +CVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMREwDwYDVQQLDAhNeVJvb3RD +QTERMA8GA1UEAwwITXlSb290Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCnBwSOYVJw47IoMHMXTVDtOYvUt3rqsurEhFcB4O8xmf2mmwr6m7s8A5Ft +AvAehg1GvnXT3t/KiyU7BK+acTwcErGyZwS2wvdB0lpHWSpOn/u5y+4ZETvQefcj +ZTdDOM9VN5nutpitgNb+1yL8sqSexfVbY7DnYYvFjOVBYoP/SGvM9jVjCad+0WL3 +FhuD+L8QAxzCieX3n9UMymlFwINQuEc+TDjuNcEqt+0J5EgS1fwzxb2RCVL0TNv4 +9a71hFGCNRj20AeZm99hbdufm7+0AFO7ocV5q43rLrWFUoBzqKPYIjga/cv/UdWZ +c5RLRXw3JDSrCqkf/mOlaEhNPlmWRF9MSus5Da3wuwgGCaVzmrf30rWR5aHHcscG +e+AOgJ4HayvBUQeb6ZlRXc0YlACiLToMKxuyxDyUcDfVEXpUIsDILF8dkiVQxEU3 +j9g6qjXiqPVdNiwpqXfBKObj8vNCzORnoHYs8cCgib3RgDVWeqkDmlSwlZE7CvQh +U4Loj4l7813xxzYEKkVaT1JdXPWu42CG/b4Y/+f4V+3rkJkYzUwndX6kZNksIBai +phmtvKt+CTdP1eAbT+C9AWWF3PT31+BIhuT0u9tR8BVSkXdQB8dG4M/AAJcTo640 +0mdYYOXT153gEKHJuUBm750ZTy+r6NjNvpw8VrMAakJwHqnIdQIDAQABo2MwYTAd +BgNVHQ4EFgQUP3SR9TmlzmXjxMe7QDKP1I2ke6EwHwYDVR0jBBgwFoAUP3SR9Tml +zmXjxMe7QDKP1I2ke6EwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggIBAFMFv4C+I0+xOAb9v6G/IOpfPBZ1ez31EXKJJBra +lulP4nRHQMeb310JS8BIeQ3dl+7+PkSxPABZSwc3jkxdSMvhc+Z4MQtTgos+Qsjs +gH7sTqwWeeQ0lHYxWmkXijrh5OPRZwTKzYQlkcn85BCUXl2KDuNEdiqPbDTao+lc +lA0/UAvC6NCyFKq/jqf4CmW5Kx6yG1v1LaE+IXn7cbIXj+DaehocVXi0wsXqj03Q +DDUHuLHZP+LBsg4e91/0Jy2ekNRTYJifSqr+9ufHl0ZX1pFDZyf396IgZ5CQZ0PJ +nRxZHlCfsxWxmxxdy3FQSE6YwXhdTjjoAa1ApZcKkkt1beJa6/oRLze/ux5x+5q+ +4QczufHd6rjoKBi6BM3FgFQ8As5iNohHXlMHd/xITo1Go3CWw2j9TGH5vzksOElK +B0mcwwt2zwNEjvfytc+tI5jcfGN3tiT5fVHS8hw9dWKevypLL+55Ua9G8ZgDHasT +XFRJHgmnbyFcaAe26D2dSKmhC9u2mHBH+MaI8dj3e7wNBfpxNgp41aFIk+QTmiFW +VXFED6DHQ/Mxq93ACalHdYg18PlIYClbT6Pf2xXBnn33YPhn5xzoTZ+cDH/RpaQp +s0UUTSJT1UTXgtXPnZWQfvKlMjJEIiVFiLEC0sgZRlWuZDRAY0CdZJJxvQp59lqu +cbTm +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client-no-dist-points.cert.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client-no-dist-points.cert.pem new file mode 100644 index 000000000..038eec790 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client-no-dist-points.cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFdTCCA12gAwIBAgICEAUwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL +DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X +DTIzMDExODEyMzY1NloXDTMzMDQyNTEyMzY1NlowgYQxCzAJBgNVBAYTAlNFMRIw +EAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAcMCVN0b2NraG9sbTESMBAGA1UECgwJ +TXlPcmdOYW1lMRkwFwYDVQQLDBBNeUludGVybWVkaWF0ZUNBMR4wHAYDVQQDDBVj +bGllbnQtbm8tZGlzdC1wb2ludHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCYQqNF7o20tEwyXphDgtwkZ628baYzQoCmmaufR+5SPQWdTN+GFeApv0dP +4y/ncZV24rgButMo73e4+wPsILwSGhaVIU0mMaCmexyC4W6INBkQsVB5FAd/YM0O +gdxS6A42h9HZTaAJ+4ftgFdOOHiP3lwicXeIYykAE7Y5ikxlnHgi8p1PTLowN4Q+ +AjuXChRzmU16cUEAevZKkTVf7VCcK66aJsxBsxfykkGHhc6qLqmlMt6Te6DPCi/R +KP/kARTDWNEkp6qtpvzByYFYAKPSZxPuryajAC3RLuGNkVSB+PZ6NnZW6ASeTdra +Lwuiwsi5XPBeFb0147naQOBzSGG/AgMBAAGjggEHMIIBAzAJBgNVHRMEAjAAMBEG +CWCGSAGG+EIBAQQEAwIFoDBBBglghkgBhvhCAQ0ENBYyT3BlblNTTCBHZW5lcmF0 +ZWQgQ2xpZW50IENlcnRpZmljYXRlIChubyBDUkwgaW5mbykwHQYDVR0OBBYEFBiV +sjDe46MixvftT/wej1mxGuN7MB8GA1UdIwQYMBaAFExwhjsVUom6tQ+Sqq6xMUET +vnPzMA4GA1UdDwEB/wQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH +AwQwMQYIKwYBBQUHAQEEJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0 +Ojk4NzcwDQYJKoZIhvcNAQELBQADggIBAKBEnKYVLFtZb3MI0oMJkrWBssVCq5ja +OYomZ61I13QLEeyPevTSWAcWFQ4zQDF/SWBsXjsrC+JIEjx2xac6XCpxcx3jDUgo +46u/hx2rT8tMKa60hW0V1Dk6w8ZHiCe94BlFLsWFKnn6dVzoJd2u3vgUaleh3uxF +hug8XY+wmHd36rO0kVe3DrsqdIdOfhMiJLDxU0cBA79vI5kCvqB8DIwCWtOzkA82 +EPl3Iws5NPhuFAR9u0xOQu0akzmSJFcEGLZ4qfatHD/tZGRduyFvMKy5iIeMzuEs +2etm01tfLHqgKGOKp5LjPm7Aoac/GeVoTvctGF+wayvOuYE7inlGZToz3kQMMzHZ +ZGBBgOhXbR2y74QoFv6DUqmmTRbGfiLYyErA5r881ntgciQi02xrGjoAFntvKb+H +HNB22Qprz16OmdC9dJKF2RhO6Cketdhv65wFWw6xlhRMCWYPY3CI8tWkxS4A4yit +RZQZg3yaeHXMaCAu5HxuqAQXKGjz+7w7N6diwbT7o7CfKk8iHUrGfkQ5nCS0GZ1r +lU1vgKtdzVvJ6HmBrCRcdNqh/L/wdIltwI/52j+TKRtELM1qHuLAYmhcRBW+2wuH +ewaNA9KEgEk6JC+iR8uOBi0ZLkMIm47j+ZLJRJVUfgkVEEFjyiYSFfpwwcgT+/Aw +EczVZOdUEbDM +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client-no-dist-points.key.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client-no-dist-points.key.pem new file mode 100644 index 000000000..02b865f5e --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client-no-dist-points.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCYQqNF7o20tEwy +XphDgtwkZ628baYzQoCmmaufR+5SPQWdTN+GFeApv0dP4y/ncZV24rgButMo73e4 ++wPsILwSGhaVIU0mMaCmexyC4W6INBkQsVB5FAd/YM0OgdxS6A42h9HZTaAJ+4ft +gFdOOHiP3lwicXeIYykAE7Y5ikxlnHgi8p1PTLowN4Q+AjuXChRzmU16cUEAevZK +kTVf7VCcK66aJsxBsxfykkGHhc6qLqmlMt6Te6DPCi/RKP/kARTDWNEkp6qtpvzB +yYFYAKPSZxPuryajAC3RLuGNkVSB+PZ6NnZW6ASeTdraLwuiwsi5XPBeFb0147na +QOBzSGG/AgMBAAECggEACSMuozq+vFJ5pCgzIRIQXgruzTkTWU4rZFQijYuGjN7m +oFsFqwlTC45UHEI5FL2nR5wxiMEKfRFp8Or3gEsyni98nXSDKcCesH8A5gXbWUcv +HeZWOv3tuUI47B709vDAMZuTB2R2L0MuFB24n5QaACBLDTIcB05UHpIQRIG9NffH +MhxqFB2kuakp67VekYGZkBCNkqfL3VQZIGRpQC8SvpnRXELqZgI4MyJgvkK6myWj +Vtpwm8YiOQoJHJx4raoVfS2NWTsCwL0M0aXMMtmM2QfMP/xB9OifxnmDDBs7Tie8 +0Wri845xLTCYthaU8B06rhoQdKXoqKmQMoF2doPm8QKBgQDN+0E0PtPkyxIho8pV +CsQnmif91EQQqWxOdkHbE96lT0UKu6ziBSbB4ClRHYil5c8p7INxRpj7pruOY3Kw +MAcacIMMBNhLBJL4R0hr/pwr18WOZxCIMcLHTaCfbVqL71TKp4/6C+GexZfaYJ46 +IZEpLU5RPmD4f9MPIDDm6KcPxwKBgQC9O9TOor93g+A4sU54CGOqvVDrdi5TnGF8 +YdimvUsT20gl2WGX5vq3OohzZi7U8FuxKHWpbgh2efqGLcFsRNFZ/T0ZXX4DDafN +Gzyu/DMVuFO4ccgFJNnl45w3/yFG40kL6yS8kss/iEYu550/uOZ1FjH+kJ0vjV6G +JD8q0PgOSQKBgG2i9cLcSia2nBEBwFlhoKS/ndeyWwRPWZGtykHUoqZ0ufgLiurG ++SkqqnM9eBVta8YR2Ki7fgQ8bApPDqWO+sjs6CPGlGXhqmSydG7fF7sSX1n7q8YC +Tn2M6RjSuOZQ3l37sFvUZSQAYmJfGPkyErTLI6uEu1KpnuqnJMBTR1DTAoGAIGQn +bx9oirqmHM4s0lsNRGKXgVZ/Y4x3G2VcQl5QhZuZY/ErxWaiL87zIF2zUnu6Fj8I +tPHCvRTwDxux6ih1dWPlm3vnX/psaK1q28ELtYIRwpanWEoQiktFqEghmBK7pDCh +3y15YOygptK6lfe+avhboml6nnMiZO+7aEbQzxECgYALuUM4fo1dQYmYuZIqZoFJ +TXGyzMkNGs61SMiD6mW6XgXj5h5T8Q0MdpmHkwsm+z9A/1of5cxkE6d8HCCz+dt5 +tnY7OC0gYB1+gDld8MZgFgP6k0qklreLVhzEz11TbMldifa1EE4VjUDG/NeAEtbq +GbLaw0NhGJtRCgL9Bc7i7g== +-----END PRIVATE KEY----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client-revoked.cert.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client-revoked.cert.pem new file mode 100644 index 000000000..d0a23bf2f --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client-revoked.cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL +DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X +DTIzMDExMjEzMDgxNloXDTMzMDQxOTEzMDgxNlowfTELMAkGA1UEBhMCU0UxEjAQ +BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN +eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExFzAVBgNVBAMMDmNs +aWVudC1yZXZva2VkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+R6 +PDtIxVlUoLYbDBbaVcxgoLjnWcvqL8wSqyWuqi/Y3cjuNYCziR9nR5dWajtkBjzJ +HyhgAr6gBVSRt4RRmDXoOcprK3GcpowAr65UAmC4hdH0af6FdKjKCnFw67byUg52 +f7ueXZ6t/XuuKxlU/f2rjXVwmmnlhBi5EHDkXxvfgWXJekDfsPbW9j0kaCUWCpfj +rzGbfkXqrPkslO41PYlCbPxoiRItJjindFjcQySYvRq7A2uYMGsrxv4n3rzo5NGt +goBmnGj61ii9WOdopcFxKirhIB9zrxC4x0opRfIaF/n1ZXk6NOnaDxu1LTZ18wfC +ZB979ge6pleeKoPf7QIDAQABo4IBNjCCATIwCQYDVR0TBAIwADARBglghkgBhvhC +AQEEBAMCBaAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIENsaWVu +dCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUQeItXr3nc6CZ++G9UCoq1YlQ9oowHwYD +VR0jBBgwFoAUTHCGOxVSibq1D5KqrrExQRO+c/MwDgYDVR0PAQH/BAQDAgXgMB0G +A1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDA7BgNVHR8ENDAyMDCgLqAshipo +dHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW0wMQYIKwYB +BQUHAQEEJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJ +KoZIhvcNAQELBQADggIBAIFuhokODd54/1B2JiNyG6FMq/2z8B+UquC2iw3p2pyM +g/Jz4Ouvg6gGwUwmykEua06FRCxx5vJ5ahdhXvKst/zH/0qmYTFNMhNsDy76J/Ot +Ss+VwQ8ddpEG3EIUI9BQxB3xL7z7kRQzploQjakNcDWtDt1BmN05Iy2vz4lnYJky +Kss6ya9jEkNibHekhxJuchJ0fVGlVe74MO7RNDFG7+O3tMlxu0zH/LpW093V7BI2 +snXNAwQBizvWTrDKWLDu5JsX8KKkrmDtFTs9gegnxDCOYdtG5GbbMq+H1SjWUJPV +wiXTF8/eE02s4Jzm7ZAxre4bRt/hAg7xTGmDQ1Hn+LzLn18I9LaW5ZWqSwwpgv+g +Z/jiLO9DJ/y525Cl7DLCpSFoDTWlQXouKhcgALcVay/cXCsZ3oFZCustburLiJi/ +zgBeEk1gVpwljriJLeZifyfWtJx6yfgB/h6fid8XLsGRD+Yc8Tzs8J1LIgi+j4ZT +UzKX3B85Kht/dr43UDMtWOF3edkOMaJu7rcg5tTsK+LIyHtXvebKPVvvA9f27Dz/ +4gmhAwwqS87Xv3FMVhZ03DNOJ6XAF+T6OTEqwYs+iK56IMSl1Jy+bCzo0j5jZVbl +XFwGxUHzM7pfM6PDx657oUxG1QwM/fIWA18F+kY/yigXxq6pYMeAiQsPanOThgHp +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client-revoked.key.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client-revoked.key.pem new file mode 100644 index 000000000..0b7698da9 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client-revoked.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCz5Ho8O0jFWVSg +thsMFtpVzGCguOdZy+ovzBKrJa6qL9jdyO41gLOJH2dHl1ZqO2QGPMkfKGACvqAF +VJG3hFGYNeg5ymsrcZymjACvrlQCYLiF0fRp/oV0qMoKcXDrtvJSDnZ/u55dnq39 +e64rGVT9/auNdXCaaeWEGLkQcORfG9+BZcl6QN+w9tb2PSRoJRYKl+OvMZt+Reqs ++SyU7jU9iUJs/GiJEi0mOKd0WNxDJJi9GrsDa5gwayvG/ifevOjk0a2CgGacaPrW +KL1Y52ilwXEqKuEgH3OvELjHSilF8hoX+fVleTo06doPG7UtNnXzB8JkH3v2B7qm +V54qg9/tAgMBAAECggEAAml+HRgjZ+gEezot3yngSBW7NvR7v6e9DmKDXpGdB7Go +DANBdGyzG5PU9/AGy9pbgzzl6nnJXcgOD7w8TvRifrK8WCgHa1f05IPMj458GGMR +HlQ8HX647eFEgkLWo4Z6tdB1VM2geDtkNFmn8nJ+wgAYgIdSWPOyDOUi+B43ZbIN +eaLWkP2fiX9tcJp41cytW+ng2YIm4s90Nt4FJPNBNzOrhVm35jciId02MmEjCEnr +0YbK9uoMDC2YLg8vhRcjtsUHV2rREkwEAQj8nCWvWWheIwk943d6OicGAD/yebpV +PTjtlZlpIbrovfvuMcoTxJg3WS8LTg/+cNWAX5a3eQKBgQDcRY7nVSJusYyN0Bij +YWc9H47wU+YucaGT25xKe26w1pl6s4fmr1Sc3NcaN2iyUv4BuAvaQzymHe4g9deU +D9Ws/NCQ9EjHJJsklNyn2KCgkSp7oPKhPwyl64XfPdV2gr5AD6MILf7Rkyib5sSf +1WK8i25KatT7M4mCtrBVJYHNpQKBgQDREjwPIaQBPXouVpnHhSwRHfKD0B1a2koq +4VE6Fnf3ogkiGfV9kqXwIfPHL0tfotFraM3FFmld8RcxhKUPr4oj+K9KTxmMD9lm +9Hal0ANXYmHs5a1iHyoNmTpBGHALWLT9fCoeg+EIYabi2+P1c7cDIdUPkEzo4GmI +nCIpv7hGqQKBgEFUC+8GK+EinWoN1tDV+ZWCP5V9fJ43q1E7592bQBgIfZqLlnnP +dEvVn6Ix3sZMoPMHj9Ra7qjh5Zc28ooCLEBS9tSW7uLJM44k7FCHihQ1GaFy+aLj +HTA0aw7rutycKCq9uH+bjKDBgWDDj3tMAS2kOMCvcJ1UCquO3TtTlWzVAoGBAIDN +8yJ/X0NEVNnnkKZTbWq+QILk3LD0e20fk6Nt5Es0ENxpkczjZEglIsM8Z/trnAnI +b71UqWWu+tMPHYIka77tn1DwmpSnzxCW2+Ib3XMgsaP5fHBPMuFd3X3tSFo1NIxW +yrwyE5nOT7rELhUyTTYoydLk2/09BMedKY7/BtDBAoGAXeX1pX74K1i/uWyYKwYZ +sskRueSo9whDJuZWgNiUovArr57eA+oA+bKdFpiE419348bkFF8jNoGFQ6MXMedD +LqHAYIj+ZPIC4+rObHqO5EaIyblgutwx3citkQp7HXDBxojnOKA9mKQXj1vxCaL1 +/1fFNJQCzEqwnKwnhI2MJ28= +-----END PRIVATE KEY----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client.cert.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client.cert.pem new file mode 100644 index 000000000..b37d1b0ba --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client.cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFljCCA36gAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL +DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X +DTIzMDExMjEzMDgxNloXDTMzMDQxOTEzMDgxNlowdzELMAkGA1UEBhMCU0UxEjAQ +BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN +eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExETAPBgNVBAMMCE15 +Q2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvGuAShewEo8V +/+aWVO/MuUt92m8K0Ut4nC2gOvpjMjf8mhSSf6KfnxPklsFwP4fdyPOjOiXwCsf3 +1QO5fjVr8to3iGTHhEyZpzRcRqmw1eYJC7iDh3BqtYLAT30R+Kq6Mk+f4tXB5Lp/ +2jXgdi0wshWagCPgJO3CtiwGyE8XSa+Q6EBYwzgh3NFbgYdJma4x+S86Y/5WfmXP +zF//UipsFp4gFUqwGuj6kJrN9NnA1xCiuOxCyN4JuFNMfM/tkeh26jAp0OHhJGsT +s3YiUm9Dpt7Rs7o0so9ov9K+hgDFuQw9HZW3WIJI99M5a9QZ4ZEQqKpABtYBl/Nb +VPXcr+T3fQIDAQABo4IBNjCCATIwCQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMC +BaAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIENsaWVudCBDZXJ0 +aWZpY2F0ZTAdBgNVHQ4EFgQUOIChBA5aZB0dPWEtALfMIfSopIIwHwYDVR0jBBgw +FoAUTHCGOxVSibq1D5KqrrExQRO+c/MwDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQW +MBQGCCsGAQUFBwMCBggrBgEFBQcDBDA7BgNVHR8ENDAyMDCgLqAshipodHRwOi8v +bG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW0wMQYIKwYBBQUHAQEE +JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJKoZIhvcN +AQELBQADggIBAE0qTL5WIWcxRPU9oTrzJ+oxMTp1JZ7oQdS+ZekLkQ8mP7T6C/Ew +6YftjvkopnHUvn842+PTRXSoEtlFiTccmA60eMAai2tn5asxWBsLIRC9FH3LzOgV +/jgyY7HXuh8XyDBCDD+Sj9QityO+accTHijYAbHPAVBwmZU8nO5D/HsxLjRrCfQf +qf4OQpX3l1ryOi19lqoRXRGwcoZ95dqq3YgTMlLiEqmerQZSR6iSPELw3bcwnAV1 +hoYYzeKps3xhwszCTz2+WaSsUO2sQlcFEsZ9oHex/02UiM4a8W6hGFJl5eojErxH +7MqaSyhwwyX6yt8c75RlNcUThv+4+TLkUTbTnWgC9sFjYfd5KSfAdIMp3jYzw3zw +XEMTX5FaLaOCAfUDttPzn+oNezWZ2UyFTQXQE2CazpRdJoDd04qVg9WLpQxLYRP7 +xSFEHulOPccdAYF2C45yNtJAZyWKfGaAZIxrgEXbMkcdDMlYphpRwpjS8SIBNZ31 +KFE8BczKrg2qO0ywIjanPaRgrFVmeSvBKeU/YLQVx6fZMgOk6vtidLGZLyDXy0Ff +yaZSoj+on++RDz1IXb96Y8scuNlfcYI8QeoNjwiLtf80BV8SRJiG4e/jTvMf/z9L +kWrnDWvx4xkUmxFg4TK42dkNp7sEYBTlVVq9fjKE92ha7FGZRqsxOLNQ +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client.key.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client.key.pem new file mode 100644 index 000000000..2e767d81f --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC8a4BKF7ASjxX/ +5pZU78y5S33abwrRS3icLaA6+mMyN/yaFJJ/op+fE+SWwXA/h93I86M6JfAKx/fV +A7l+NWvy2jeIZMeETJmnNFxGqbDV5gkLuIOHcGq1gsBPfRH4qroyT5/i1cHkun/a +NeB2LTCyFZqAI+Ak7cK2LAbITxdJr5DoQFjDOCHc0VuBh0mZrjH5Lzpj/lZ+Zc/M +X/9SKmwWniAVSrAa6PqQms302cDXEKK47ELI3gm4U0x8z+2R6HbqMCnQ4eEkaxOz +diJSb0Om3tGzujSyj2i/0r6GAMW5DD0dlbdYgkj30zlr1BnhkRCoqkAG1gGX81tU +9dyv5Pd9AgMBAAECggEAAifx6dZKIeNkQ8OaNp5V2IKIPSqBOV4/h/xKMkUZXisV +eDmTCf8du0PR7hfLqrt9xYsGDv+6FQ1/8K231l8qR0tP/6CTl/0ynM4qqEAGeFXN +3h2LvM4liFbdjImechrcwcnVaNKg/DogT5zHUYSMtB/rokaG0VBO3IX/+SGz0aXi +LOLAx6SPaLOVX9GYUCiigTSEDwaQA+F3F6J2fR4u8PrXo+OQUqxjQ/fGXWp+4IfA +6djlpvzO2849/WPB1tL20iLXJlL2OL0UgQNtbKWTjexMe+wgCR5BzCwTyPsQvMwX +YOQrTOwgF3b6O+gLks5wSRT0ivq1sKgzA534+X4M+wKBgQDirPTLlrYobOO8KUpV +LOJU8x9leiRNU9CZWrW/mOw/BXGXikqNWvgL595vvADsjYciuRxSqEE7lClB8Pp9 +20TMlES9orx7gdoQJCodpNV1BuBJhE9YtUiXzWAj+7m3D9LsXM1ewW/2A7Vvopj3 +4zKY7uHAFlo3nXwLOfChG5/i9wKBgQDUy5fPFa58xmn7Elb6x4vmUDHg6P4pf75E +XHRQvNA8I7DTrpqfcsF1N4WuJ3Lm//RSpw7bnyqP20GoEfGHu/iCUPf29B7CuXhO +vvD+I8uPdn8EcKUBWV+V0xNQN/gCe0TzrEjAkZcO2Lq0j93R8HVl3BbowxgRvQV9 +GmxQG/boKwKBgFeV8uSzsGEAaiKrZbBxrmaappgEUQCcES8gULfes/JJ/TFL2zCx +ZMTc7CMKZuUAbqXpFtuNbd9CiYqUPYXh8ryF0eXgeqnSa9ruzmMz7NLSPFnLyQkC +yzD0x2BABOuKLrrrxOMHJWbO2g1vq2GlJUjYjNw3BtcUf/iqg6MM1IPTAoGAWYWJ +SSqS7JVAcsrFYt1eIrdsNHVwr565OeM3X9v/Mr3FH1jeXeQWNSz1hU29Ticx7y+u +1YBBlKGmHoHl/bd7lb9ggjkzU7JZRa+YjSIb+i/cwc5t7IJf7xUMk/vnz4tyd5zs +Qm89gJZ2/Y1kwXSKvx53WNbyokvGKlpaZN1O418CgYACliGux77pe4bWeXSFFd9N +50ipxDLVghw1c5AiZn25GR5YHJZaV4R0wmFcHdZvogLKi0jDMPvU69PaiT8eX/A1 +COkxv7jY1vtKlEtb+gugMjMN8wvb2va4kyFamjqnleiZlBSqIF/Y17wBoMvaWgZ0 +bEPCN//ts5hBwgb1TwGrrg== +-----END PRIVATE KEY----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client1.cert.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client1.cert.pem new file mode 100644 index 000000000..4e41c15bb --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client1.cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFljCCA36gAwIBAgICEAowDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL +DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X +DTIzMDMxNjIwMjAzMloXDTMzMDYyMTIwMjAzMlowdjELMAkGA1UEBhMCU0UxEjAQ +BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN +eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEDAOBgNVBAMMB0Ns +aWVudDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDcDhlEvUIYc9uA +ocOBXt5thKrovs+8V0Eus/WrHMTKBk0Kw4X+7HBaRBoZj2sZpYfN63lVaO75kW4I +uJuorGj5PAXYWJj+4uAsCc95xAN/liCuHJnxE5togWVt8W+z0Zll98RIpiCohqiE +FLDL4X6FREL07GLgQZ/BFORvAwU+Gog05AFh43iZDnJl8MmrG2HBSRXtSZ6vQj9A +NrOSqz5eK4YIHEEsgwTWQmhtNwu3Y+GzrAPWCA4TeYrSRwIrnGh20fOWXkAMldS4 +eRXmBztEsXMGqbe6oYO1QPYOlmoGO8EaaDPJ2sFIuM0zn98Alq3kCnRhM5Bi9RpJ +7IpudIopAgMBAAGjggE3MIIBMzAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIF +oDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgQ2xpZW50IENlcnRp +ZmljYXRlMB0GA1UdDgQWBBQoIuXq3wG6JEzAEj9wPe7am0OVgjAfBgNVHSMEGDAW +gBRMcIY7FVKJurUPkqqusTFBE75z8zAOBgNVHQ8BAf8EBAMCBeAwHQYDVR0lBBYw +FAYIKwYBBQUHAwIGCCsGAQUFBwMEMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9s +b2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUxLmNybC5wZW0wMQYIKwYBBQUHAQEE +JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJKoZIhvcN +AQELBQADggIBAHqKYcwkm3ODPD7Mqxq3bsswSXregWfc8tqfIBc5FZg2F+IzhxcJ +kINB0lmcNdLALK6ka0sDs1Nrj1KB96NcHUqE+WY/qPS1Yksr34yFatb1ddlKQ9HK +VRrIsi0ZfjBpHpvoQ0GsLeyRKm7iN/Fm5H9u8rw6RBu0Oe/l20FVSQIDzldYw51L +uV/E9No8ZhdQ2Dffujs8madI7b7I1NMXS+Z1pZ+gYrz6O60tDEprE+rYuYWypURr +fK+DnLLl+KQ+eekTPynw7LRpFzI/1cOMmd4BRnsBHCbCObfNp7WPasemZOEXGIlZ +CQwZS62DYOJE4u4Nz5pSF+JgXfr6X/Im6Y1SV900xVHfoL0GpFDI9k+0Y5ncHfSH ++V9HlRWB3zqQF+yla32XOpBbER0vFDH52gp8/o1ZGg7rr6KrP4QKxnqywNLiAPDX +txaAykZhON7uG8j+Lbjx5Ik91NRn9Fd5NH/vtT33a4uig2TP9EWd7EPcD2z8ONuD +yiK3S37XAnmSKKX4HcCpEb+LedtqQo/+sqWyWXkpKdpkUSozvcYS4J/ob3z9N2IE +qIH5I+Mty1I4EB4W89Pem8DHNq86Lt0Ea6TBtPTV8NwR5aG2vvLzb5lNdpANXYcp +nGr57mTWaHnQh+yqgy66J++k+WokWkAkwE989AvUfNoQ+Jr6cTH8nKo2 +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client1.key.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client1.key.pem new file mode 100644 index 000000000..b355a3814 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client1.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcDhlEvUIYc9uA +ocOBXt5thKrovs+8V0Eus/WrHMTKBk0Kw4X+7HBaRBoZj2sZpYfN63lVaO75kW4I +uJuorGj5PAXYWJj+4uAsCc95xAN/liCuHJnxE5togWVt8W+z0Zll98RIpiCohqiE +FLDL4X6FREL07GLgQZ/BFORvAwU+Gog05AFh43iZDnJl8MmrG2HBSRXtSZ6vQj9A +NrOSqz5eK4YIHEEsgwTWQmhtNwu3Y+GzrAPWCA4TeYrSRwIrnGh20fOWXkAMldS4 +eRXmBztEsXMGqbe6oYO1QPYOlmoGO8EaaDPJ2sFIuM0zn98Alq3kCnRhM5Bi9RpJ +7IpudIopAgMBAAECggEARcly2gnrXDXh9vlWN0EO6UyZpxZcay6AzX7k+k81WZyF +8lPvutjhCL9wR4rkPE3ys6tp31xX7W3hp4JkWynSYLhYYjQ20R7CWTUDR2qScXP7 +CTyo1XuSXaIruKJI+o4OR/g7l46X7NpHtxuYtg/dQAZV9bbB5LzrHSCzEUGz9+17 +jV//cBgLBiMdlbdLuQoGt4NQpBkNrauBVFq7Nq648uKkICmUo3Bzn/dfn3ehB+Zc ++580S+tawYd224j19tFQmd5oK8tfjqKuHenNGjp/gsRoY86N7qAtc3VIQ0yjE6ez +tgREo/ftCb8kGfwRJOAQIeeDamBv+FWNT6QzcOtbwQKBgQDzWhY9BUgI8JVzjYg0 +oWfU90On81BtckKsEo//8MTlgwOD2PnUF0hTZF3RcSPYT+HybouTqHT8EOLBAzqy +1+koH06MnAc/Y2ipaAe2fGaVH3SuXAsV/b8VcWWl4Qx7tYJDhE7sKmdl3/+jHZ7A +hZQzgOQnxxCANBo3pwF9KboDbwKBgQDnfglSpgYdGzFpWp1hZnPl2RKIfX/4M2z2 +s+hVN1tp+1VySPrBRUC3J6hKPQUzzeUzPICclHOnO+kP7jAos/rlJ9VcNdAQTbTL +7Ds9Em1KJTBphE038YbW3e93rydQpukXh50wRD9RI/3F3A/1rKhab92DXZZr6Wqu +JouhNV8f5wKBgQCLQ3XQi/Iyc4QDse5NuETUgoCsX7kaOTZghOr1nFMByV08mfI2 +5vAUES8DigzqYKS8eXjVEqWIDx3FOVThPmCG/ouUOkKHixs9P3SSgVSvaGX81l3d +wu4UlmWGbWkYbsJSYyhLTOUJTwxby7qrEIbEhrGK9gfCZo7OZHucpkF2bwKBgFhl +1qWK5JbExY+XnLWO6/7/b4ZTdkSPTrK+bJ/t7aiA41Yq7CZVjarjJ+6BcrUfkMCK +AArK3Yck55C/wgApCkvrdBwsKHGxWrLsWIqvuLAxl1UTwnD0eCsgwMsRRZAUzLnB +fZLq3MrdVZDywd1suzUdtpbta/11OtmZuoQq31JNAoGAIzmevuPaUZRqnjDpLXAm +Bo11q6CunhG5qZX4wifeZ9Fp5AaQu97F36igZ5/eDxFkDCrFRq6teMiNjRQZSLA3 +5tMBkq6BtN2Ozzm/6D135c4BF14ODHqAMPUy4sXbw5PS/kRFs4fKAH/+LcAOPgyI +N/jJIY1LfM7PzrG2NdyscMU= +-----END PRIVATE KEY----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client2.cert.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client2.cert.pem new file mode 100644 index 000000000..0cba3fb26 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client2.cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFljCCA36gAwIBAgICEAswDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL +DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X +DTIzMDMxNjIwMjAzMloXDTMzMDYyMTIwMjAzMlowdjELMAkGA1UEBhMCU0UxEjAQ +BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN +eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEDAOBgNVBAMMB0Ns +aWVudDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFLcCjzNhfY6Sk +2nSdrB/6UPPeTCCH5NBBVLvu1hdlqLS4qEdq8+EjyMDZTtspRtYPkBfjpOrlBWUO +lKyxw2mZOjZ8iWvd4sJaAI/6KZl5X0Rdsg1RjzW03kUdLx9XJCyrYY0YFrT1dgJo +Ih56jk2SJX7wrz0NCJ05VPIdpaOF6CcziA+YhdVHcE6xyHagsYI0JdDWxFZrl9zT +LyhaDgBUN/yUQBnxKzxs8TMT4YVSi73ouh5IP9Xvs52hd6HO8ZGVr+YthQZKo95p +OlwFF+AQWxdDIKoPYUPFo8XMOXvOeQ9iUJarxrYSrelLXtGkaGLBolAvqo/YKE7j +rcJWjRGHAgMBAAGjggE3MIIBMzAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIF +oDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgQ2xpZW50IENlcnRp +ZmljYXRlMB0GA1UdDgQWBBTOo9YSgx1h5k/imP7nOfRfzQrRxjAfBgNVHSMEGDAW +gBRMcIY7FVKJurUPkqqusTFBE75z8zAOBgNVHQ8BAf8EBAMCBeAwHQYDVR0lBBYw +FAYIKwYBBQUHAwIGCCsGAQUFBwMEMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9s +b2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUyLmNybC5wZW0wMQYIKwYBBQUHAQEE +JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJKoZIhvcN +AQELBQADggIBAFo91lLqjPY67Wmj2yWxZuTTuUwXdXXUQxL6sEUUnfkECvRhNyBA +eCHkfVopNbXZ5tdLfsUvXF0ulaC76GCK/P7gHOG9D/RJX/85VzhuJcqa4dsEEifg +IiKIG7viYxSA6HFXuyzHvwNco3FqTBHbY46lKf1lWRVLhiAtcwcyPP34/RWcPfQi +6NZfLyitu5U7Z9XVN5wCp8sg0ayaO5Ib2ejIYuBCUddV1gV//tSDf+rKCgtAbm/X +K64Bf3GdaX3h6EhoqMZ+Z2f4XpKSXTabsWAU44xdVxisI82eo+NwT8KleE65GpOv +nPvr/dLq5fQ6VtHbRL3wWqhzB1VKVCtd8a6RE2k8HVWflU3qgwJ+woF19ed921eq +OZxc+KzjsGFyW1D2fPdgoZFmePadSstIME7qtCNEi7D3im01/1KKzE2m/nosrHeW +ePjY2YrXu0w47re/N2kBJL2xRbj+fAjBsfNn9RhvQsWheXG6mgg8w1ac6y72ZA2W +72pWoDkgXQMX5XBBj/zMnmwtrX9zTILFjNGFuWMPYgBRI0xOf2FoqqZ67cQ2yTW/ +1T/6Mp0FSh4cIo/ENiNSdvlt3BIo84EyOm3iHHy28Iv5SiFjF0pkwtXlYYvjM3+R +BeWqlPsVCZXcVC1rPVDzfWZE219yghldY4I3QPJ7dlmszi8eI0HtzhTK +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client2.key.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client2.key.pem new file mode 100644 index 000000000..29196b1e2 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client2.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFLcCjzNhfY6Sk +2nSdrB/6UPPeTCCH5NBBVLvu1hdlqLS4qEdq8+EjyMDZTtspRtYPkBfjpOrlBWUO +lKyxw2mZOjZ8iWvd4sJaAI/6KZl5X0Rdsg1RjzW03kUdLx9XJCyrYY0YFrT1dgJo +Ih56jk2SJX7wrz0NCJ05VPIdpaOF6CcziA+YhdVHcE6xyHagsYI0JdDWxFZrl9zT +LyhaDgBUN/yUQBnxKzxs8TMT4YVSi73ouh5IP9Xvs52hd6HO8ZGVr+YthQZKo95p +OlwFF+AQWxdDIKoPYUPFo8XMOXvOeQ9iUJarxrYSrelLXtGkaGLBolAvqo/YKE7j +rcJWjRGHAgMBAAECggEABJYUCcyJcnbagytBxfnaNQUuAp8AIypFG3kipq0l5Stk +gGaTJq5F4OTGS4ofRsqeu07IgBSAfqJcJH8toPkDQqfvs6ftO1Mso2UzakMOcP51 +Ywxd91Kjm+LKOyHkHGDirPGnutUg/YpLLrrMvTk/bJHDZCM4i/WP1WTREVFjUgl7 +4L6Y53x2Lk5shJJhv0MzTGaoZzQcW0EbhNH1AI6MBv5/CN5m/7/+HCPlHSNKnozl +o3PXD6l0XNfOY2Hi6MgS/Vd70s3VmDT9UCJNsDjdFpKNHmI7vr9FScOLN8EwbqWe +maFa0TPknmPDmVjEGMtgGlJWL7Sm0MpNW+WsEXcDPQKBgQDv3sp0nVML9pxdzX/w +rGebFaZaMYDWmV9w0V1uXYh4ZkpFmqrWkq/QSTGpwIP/x8WH9FBDUZqspLpPBNgG +ft1XhuY34y3hoCxOyRhQcR/1dY+lgCzuN4G4MG3seq/cAXhrmkPljma/iO8KzoRK +Pa+uaKFGHy1vWY2AmOhT20zr4wKBgQDScA3478TFHg9THlSFzqpzvVn5eAvmmrCQ +RMYIZKFWPortqzeJHdA5ShVF1XBBar1yNMid7E7FXqi/P8Oh+E6Nuc7JxyVIJWlV +mcBE1ceTKdZn7A0nuQIaU6hcn7xz/UHmxGur1ZcNQm3diFJ2CPn11lzZlkSZLSCN +V86nndA9DQKBgQCWsUxXPo7xsRhDBdseg/ECyPMdLoRWTTxcT+t2bmRR31FBsQ0q +iDTTkWgV0NAcXJCH/MB/ykB1vXceNVjRm9nKJwFyktI8MLglNsiDoM4HErgPrRqM +/WoNIL+uFNVuTa4tS1jkWjXKlmg2Tc9mJKK92xWWS/frQENZSraKF/eXKQKBgGR9 +ni6CUTTQZgELOtGrHzql8ZFwAj7dH/PE48yeQW0t8KoOWTbhRc4V0pLGmhSjJFSl +YCgJ8JPP4EVz7bgrG1gSou04bFVHiEWYZnh4nhVopTp7Psz5TEfGK2AP5658Ajxx +D/m+xaNPVae0sawsHTGIbE57s8ZyBll41Pa2JfsBAoGBANtS7SOehkflSdry0eAZ +50Ec3CmY+fArC044hQCmXxol5SiTUnXf/OIQH8y+RZUjF4F3PbbrFNLm/J6wuUPw +XUIb4gAL75srakL8DXqyTYfO02aNrFEHhXzMs+GoAnRkFy16IAAFwpjbYSPanfed +PfHCTWz6Y0pGdh1hUJAFV/3v +-----END PRIVATE KEY----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client3.cert.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client3.cert.pem new file mode 100644 index 000000000..94092fad9 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client3.cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFljCCA36gAwIBAgICEAwwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL +DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X +DTIzMDMxNjIwMjAzMloXDTMzMDYyMTIwMjAzMlowdjELMAkGA1UEBhMCU0UxEjAQ +BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN +eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEDAOBgNVBAMMB0Ns +aWVudDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEOZ6fYNjZDNXX +eOyapHMOMeNeYM3b7vsWXAbiJIt4utVrTS0A+/G640t/U0g8F9jbKgbjEEPtgPJ7 +GltjLWObfqDWKSO2D9/ei2+NauqgiN/HX+dQnSKHob0McXBXvLfrA4tn4braKrbg +p1fZB8bAECuT/bUhVBqWlzrUwDMpqjMJWDab48ixezb2gnc/ePE6wq/d3ecDb0/k +cYWQ0LX4JiQBgaTGhwczyoGfL1z2vx5kJqptK+r0Hc2jNCn6kFvoZUCYjCWgWNxZ +sQk7fObQQkUb/XQyqRaKJBWDyqsNcuK2gOg3LGeolAlgtMiEqGhHv77XdJnJug/w +3OiHpP/7AgMBAAGjggE3MIIBMzAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIF +oDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgQ2xpZW50IENlcnRp +ZmljYXRlMB0GA1UdDgQWBBRxZFdIkSg6zDZCakXmIest5a6dBzAfBgNVHSMEGDAW +gBRMcIY7FVKJurUPkqqusTFBE75z8zAOBgNVHQ8BAf8EBAMCBeAwHQYDVR0lBBYw +FAYIKwYBBQUHAwIGCCsGAQUFBwMEMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9s +b2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUzLmNybC5wZW0wMQYIKwYBBQUHAQEE +JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJKoZIhvcN +AQELBQADggIBAEntkhiPpQtModUF/ffnxruq+cqopPhIdMXhMD8gtU5e4e7o3EHX +lfZKIbxyw56v6dFPrl4TuHBiBudqIvBCsPtllWKixWvg6FV3CrEeTcg4shUIaJcD +pqv1qHLwS4pue6oau/lb8jv1GuzuBXoMFQwlmiOXO7xXqXjV2GdmkFJCDdB/0BW1 +VHvh0DXgotaxITWKhCpSNB7F7LSvegRwZIAN6JXrLDpue7tgqLqBB1EzpmS6ALbn +uZDdikOs/tGAFB3un/3Gl7jEPL8UGOoSj/H9PUT5AFHrHJDH72+QSXu09agz8RWJ +V939njYFCAxQ8Jt2mOK8BJQDJgPtLfIIb1iYicQV13Eypt8uIUYvp0i0Wq8WxPbq +rOEvQYpcGUsreS5XqZ7y68hgq6ePiR18Fnc3GyTV5o6qT3W7IOvPArTzNV5fFCwM +lx8xSEm+ebJrJPphp6Uc/h8evohvAN8R/Z7FSo9OL6V+F3ywPqWTXaqiIiRc9PS0 +0vxsYZ96EeZY5HzjN6LzHxmkv4KYM5I1qmXlviQlaU+sotp3tzegADlM4K78nUFh +HuXamecEcS73eAgjk+FGqJ9E25B0TLlQMcP6tCKdaUIGn6ZsF5wT87GzqT99wL/5 +foHCYIkyG7ZmAQmoaKBd4q6xqVOUHovmsPza69FuSrsBxoRR39PtAnrY +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/client3.key.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/client3.key.pem new file mode 100644 index 000000000..6ede63fd2 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/client3.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEOZ6fYNjZDNXX +eOyapHMOMeNeYM3b7vsWXAbiJIt4utVrTS0A+/G640t/U0g8F9jbKgbjEEPtgPJ7 +GltjLWObfqDWKSO2D9/ei2+NauqgiN/HX+dQnSKHob0McXBXvLfrA4tn4braKrbg +p1fZB8bAECuT/bUhVBqWlzrUwDMpqjMJWDab48ixezb2gnc/ePE6wq/d3ecDb0/k +cYWQ0LX4JiQBgaTGhwczyoGfL1z2vx5kJqptK+r0Hc2jNCn6kFvoZUCYjCWgWNxZ +sQk7fObQQkUb/XQyqRaKJBWDyqsNcuK2gOg3LGeolAlgtMiEqGhHv77XdJnJug/w +3OiHpP/7AgMBAAECggEADSe89sig5E63SKAlFXcGw0H2XgqIzDP/TGMnqPvNoYhX +eSXUgxDhBptpB9e9a4RaKwaFxxPjlSXEdYFX9O22YSN1RMMl6Q8Zl9g3edhcDR6W +b7Qbx2x8qj6Rjibnlh8JiFPiaDjN2wUeSDBss/9D98NkKiJ9Ue2YCYmJAOA3B3w9 +2t4Co5+3YrxkdzkvibTQCUSEwHFeB1Nim21126fknMPxyrf+AezRBRc8JNAHqzWb +4QEeMnmIJDOzc3Oh7+P85tNyejOeRm9T7X3EQ0jKXgLYe+HUzXclBQ66b9x9Nc9b +tNn6XkMlLlsQ3f149Th6PtHksH3hM+GF8bMuCp9yxQKBgQDGk0PYPkLqTD8jHjJW +s8wBNhozigZPGaynxdTsD7L6UtDdOl1sSW/jFOj9UIs1duBce9dP1IjFc0jY+Kin +lMLv3qCtk5ZjxRglOoLipR9hdClcM69rDoRZdoQK8KYa+QHcOTSazIp3fnw4gWSX +nscelMfd1rtVP0dOGTuqE/73/QKBgQD8+F5WAi2IOVPHnBxAAOP+6XTs9Ntn1sDi +L5wNgm+QA28aJJ4KRAwdXIc3IFPlHxZI77c2K1L9dKDu9X4UcyZIZYDvGVLuOOt5 +twaRaGuJW03cjbgOWC7rGyfzfZ49YlCZi2YuxERclBkbqgWD9hfa8twUfKNguF2Y +AyiOhohtVwKBgQCJB8zUp7pzhqQ3LrpcHHzWBSi1kjTiVvxPVnSlZfwDRCz/zSv0 +8wRz9tUFIZS/E0ama4tcenTblL+bgpSX+E9BSiclQOiR9su/vQ3fK0Vpccis6LnP +rdflCKT8C68Eg/slppBHloijBzTfpWLuQlJ0JwV5b5ocrKsfGMiUiHH1XQKBgQDg +RnakfEPP7TtY0g+9ssxwOJxAZImM0zmojpsk4wpzvIeovuQap9+xvFHoztFyZhBE +07oz3U8zhE4V7TI9gSVktBEOaf47U914yIqbKd+FJJywODkBBq96I1ZVKn67X0mk +B5GtTrZo+agU/bTsHKdjp0L1KtdSLcJUviAb1Cxp+wKBgDrGqS01CCgxSUwMaZe4 +8HFWp/oMSyVDG9lTSC3uP/VL76zNFI55T3X06Q87hDN3gCJGUOmHzDZ/oCOgM4/S +SU55M4lXeSEdFe84tMXJKOv5JXTkulzBYzATJ5J8DeS/4YZxMKyPDLXX8wgwmU+l +i6Imd3qCPhh5eI3z9eSNDX+6 +-----END PRIVATE KEY----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/crl.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/crl.pem new file mode 100644 index 000000000..a119cede2 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/crl.pem @@ -0,0 +1,20 @@ +-----BEGIN X509 CRL----- +MIIDPDCCASQCAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0UxEjAQBgNV +BAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQLDBBNeUlu +dGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBFw0yMjA3MjAy +MDIzNTNaFw0zMjEwMjUyMDIzNTNaMBUwEwICEAIXDTIyMDYxMzEyNDIwNVqgbjBs +MB8GA1UdIwQYMBaAFCuv1TkzC1fSgTfzE1m1u5pRCJsVMDwGA1UdHAQ1MDOgLqAs +hipodHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW2EAf8w +CwYDVR0UBAQCAhADMA0GCSqGSIb3DQEBCwUAA4ICAQBbWdqRFsIrG6coL6ln1RL+ +uhgW9l3XMmjNlyiYHHNzOgnlBok9xu9UdaVCOKC6GEthWSzSlBY1AZugje57DQQd +RkIJol9am94lKMTjF/qhzFLiSjho8fwZGDGyES5YeZXkLqNMOf6m/drKaI3iofWf +l63qU9jY8dnSrVDkwgCguUL2FTx60v5H9NPxSctQ3VDxDvDj0sTAcHFknQcZbfvY +ZWpOYNS0FAJlQPVK9wUoDxI0LhrWDq5h/T1jcGO34fPT8RUA5HRtFVUevqSuOLWx +WTfTx5oDeMZPJTvHWUcg4yMElHty4tEvtkFxLSYbZqj7qTU+mi/LAN3UKBH/gBEN +y2OsJvFhVRgHf+zPYegf3WzBSoeaXNAJZ4UnRo34P9AL3Mrh+4OOUP++oYRKjWno +pYtAmTrIwEYoLyisEhhZ6aD92f/Op3dIYsxwhHt0n0lKrbTmUfiJUAe7kUZ4PMn4 +Gg/OHlbEDaDxW1dCymjyRGl+3/8kjy7bkYUXCf7w6JBeL2Hw2dFp1Gh13NRjre93 +PYlSOvI6QNisYGscfuYPwefXogVrNjf/ttCksMa51tUk+ylw7ZMZqQjcPPSzmwKc +5CqpnQHfolvRuN0xIVZiAn5V6/MdHm7ocrXxOkzWQyaoNODTq4js8h8eYXgAkt1w +p1PTEFBucGud7uBDE6Ub6A== +-----END X509 CRL----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/emqx.conf b/apps/emqx/test/emqx_crl_cache_SUITE_data/emqx.conf new file mode 100644 index 000000000..f34ab1456 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/emqx.conf @@ -0,0 +1,12 @@ +crl_cache.refresh_interval = {{ refresh_interval }} +crl_cache.http_timeout = 17s +crl_cache.capacity = {{ cache_capacity }} +listeners.ssl.default { + ssl_options { + keyfile = "{{ test_data_dir }}/server.key.pem" + certfile = "{{ test_data_dir }}/server.cert.pem" + cacertfile = "{{ test_data_dir }}/ca-chain.cert.pem" + verify = verify_peer + enable_crl_check = true + } +} diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/emqx_crl_cache_http_server.erl b/apps/emqx/test/emqx_crl_cache_SUITE_data/emqx_crl_cache_http_server.erl new file mode 100644 index 000000000..4e8b989fa --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/emqx_crl_cache_http_server.erl @@ -0,0 +1,67 @@ +-module(emqx_crl_cache_http_server). + +-behaviour(gen_server). +-compile([nowarn_export_all, export_all]). + +set_crl(CRLPem) -> + ets:insert(?MODULE, {crl, CRLPem}). + +%%-------------------------------------------------------------------- +%% `gen_server' APIs +%%-------------------------------------------------------------------- + +start_link(Parent, BasePort, CRLPem, Opts) -> + process_flag(trap_exit, true), + stop_http(), + timer:sleep(100), + gen_server:start_link(?MODULE, {Parent, BasePort, CRLPem, Opts}, []). + +init({Parent, BasePort, CRLPem, Opts}) -> + Tab = ets:new(?MODULE, [named_table, ordered_set, public]), + ets:insert(Tab, {crl, CRLPem}), + ok = start_http(Parent, [{port, BasePort} | Opts]), + Parent ! {self(), ready}, + {ok, #{parent => Parent}}. + +handle_call(_Request, _From, State) -> + {reply, ignored, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + stop_http(). + +stop(Pid) -> + ok = gen_server:stop(Pid). + +%%-------------------------------------------------------------------- +%% Callbacks +%%-------------------------------------------------------------------- + +start_http(Parent, Opts) -> + {ok, _Pid1} = cowboy:start_clear(http, Opts, #{ + env => #{dispatch => compile_router(Parent)} + }), + ok. + +stop_http() -> + cowboy:stop_listener(http), + ok. + +compile_router(Parent) -> + {ok, _} = application:ensure_all_started(cowboy), + cowboy_router:compile([ + {'_', [{'_', ?MODULE, #{parent => Parent}}]} + ]). + +init(Req, #{parent := Parent} = State) -> + %% assert + <<"GET">> = cowboy_req:method(Req), + [{crl, CRLPem}] = ets:lookup(?MODULE, crl), + Parent ! {http_get, iolist_to_binary(cowboy_req:uri(Req))}, + Reply = reply(Req, CRLPem), + {ok, Reply, State}. + +reply(Req, CRLPem) -> + cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, CRLPem, Req). diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/emqx_just_verify.conf b/apps/emqx/test/emqx_crl_cache_SUITE_data/emqx_just_verify.conf new file mode 100644 index 000000000..8b9549823 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/emqx_just_verify.conf @@ -0,0 +1,12 @@ +node.name = test@127.0.0.1 +node.cookie = emqxsecretcookie +node.data_dir = "{{ test_priv_dir }}" +listeners.ssl.default { + ssl_options { + keyfile = "{{ test_data_dir }}/server.key.pem" + certfile = "{{ test_data_dir }}/server.cert.pem" + cacertfile = "{{ test_data_dir }}/ca-chain.cert.pem" + verify = verify_peer + enable_crl_check = false + } +} diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate-not-revoked.crl.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate-not-revoked.crl.pem new file mode 100644 index 000000000..e484b44c0 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate-not-revoked.crl.pem @@ -0,0 +1,19 @@ +-----BEGIN X509 CRL----- +MIIDJTCCAQ0CAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0UxEjAQBgNV +BAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQLDBBNeUlu +dGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBFw0yMzAxMTIx +MzA4MTZaFw0zMzAxMDkxMzA4MTZaoG4wbDAfBgNVHSMEGDAWgBRMcIY7FVKJurUP +kqqusTFBE75z8zA8BgNVHRwENTAzoC6gLIYqaHR0cDovL2xvY2FsaG9zdDo5ODc4 +L2ludGVybWVkaWF0ZS5jcmwucGVthAH/MAsGA1UdFAQEAgIQADANBgkqhkiG9w0B +AQsFAAOCAgEAJGOZuqZL4m7zUaRyBrxeT6Tqo+XKz7HeD5zvO4BTNX+0E0CRyki4 +HhIGbxjv2NKWoaUv0HYbGAiZdO4TaPu3w3tm4+pGEDBclBj2KTdbB+4Hlzv956gD +KXZ//ziNwx1SCoxxkxB+TALxReN0shE7Mof9GlB5HPskhLorZgg/pmgJtIykEpsq +QAjJo4aq+f2/L+9dzRM205fVFegtsHvgEVNKz6iK6skt+kDhj/ks9BKsnfCDIGr+ +XnPYwS9yDnnhFdoJ40AQQDtomxggAjfgcSnqtHCxZwKJohuztbSWUgD/4yxzlrwP +Dk1cT/Ajjjqb2dXVOfTLK1VB2168uuouArxZ7KYbXwBjHduYWGGkA6FfkNJO/jpF +SL9qhX3oxcRF3hDhWigN1ZRD7NpDKwVal3Y9tmvO5bWhb5VF+3qv0HGeSGp6V0dp +sjwhIj+78bkUrcXxrivACLAXgSTGonx1uXD+T4P4NCt148dgRAbgd8sUXK5FcgU2 +cdBl8Kv2ZUjEaod5gUzDtf22VGSoO9lHvfHdpG9o2H3wC7s4tyLTidNrduIguJff +IIgc44Y252iV0sOmZ5S0jjTRiF1YUUPy9qA/6bOnr2LohbwbNZv9tDlNj8cdhxUz +cKiS+c7Qsz+YCcrp19QRiJoQae/gUqz7kmUZQgyPmDd+ArE0V+kDZEE= +-----END X509 CRL----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate-revoked-no-dp.crl.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate-revoked-no-dp.crl.pem new file mode 100644 index 000000000..4d3611d49 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate-revoked-no-dp.crl.pem @@ -0,0 +1,19 @@ +-----BEGIN X509 CRL----- +MIIC/TCB5gIBATANBgkqhkiG9w0BAQsFADBrMQswCQYDVQQGEwJTRTESMBAGA1UE +CAwJU3RvY2tob2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxGTAXBgNVBAsMEE15SW50 +ZXJtZWRpYXRlQ0ExGTAXBgNVBAMMEE15SW50ZXJtZWRpYXRlQ0EXDTIzMDExODEz +Mjc1M1oXDTMzMDExNTEzMjc1M1owFTATAgIQAhcNMjMwMTEyMTMwODE2WqAwMC4w +HwYDVR0jBBgwFoAUTHCGOxVSibq1D5KqrrExQRO+c/MwCwYDVR0UBAQCAhACMA0G +CSqGSIb3DQEBCwUAA4ICAQCxoRYDc5MaBpDI+HQUX60+obFeZJdBkPO2wMb6HBQq +e0lZM2ukS+4n5oGhRelsvmEz0qKvnYS6ISpuFzv4Qy6Vaun/KwIYAdXsEQVwDHsu +Br4m1V01igjFnujowwR/7F9oPnZOmBaBdiyYbjgGV0YMF7sOfl4UO2MqI2GSGqVk +63wELT1AXjx31JVoyATQOQkq1A5HKFYLEbFmdF/8lNfbxSCBY2tuJ+uWVQtzjM0y +i+/owz5ez1BZ/Swx8akYhuvs8DVVTbjXydidVSrxt/QEf3+oJCzTA9qFqt4MH7gL +6BAglCGtRiYTHqeMHrwddaHF2hzR61lHJlkMCL61yhVuL8WsEJ/AxVX0W3MfQ4Cw +x/A6xIkgqtu+HtQnPyDcJxyaFHtFC+U67nSbEQySFvHfMw42DGdIGojKQCeUer9W +ECFC8OATQwN2h//f8QkY7D0H3k/brrNYDfdFIcCti9iZiFrrPFxO7NbOTfkeKCt3 +7IwYduRc8DWKmS8c7j2re1KkdYnfE1sfwbn3trImkcET5tvDlVCZ1glnBQzk82PS +HvKmSjD2pZI7upfLkoMgMhYyYJhYk7Mw2o4JXuddYGKmmw3bJyHkG/Ot5NAKjb7g +k1QCeWzxO1xXm8PNDDFWMn351twUGDQ/cwrUw0ODeUZpfL0BtTn4YnfCLLTvZDxo +Vg== +-----END X509 CRL----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate-revoked.crl.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate-revoked.crl.pem new file mode 100644 index 000000000..4c5cdd441 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate-revoked.crl.pem @@ -0,0 +1,20 @@ +-----BEGIN X509 CRL----- +MIIDPDCCASQCAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0UxEjAQBgNV +BAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQLDBBNeUlu +dGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBFw0yMzAxMTIx +MzA4MTZaFw0zMzAxMDkxMzA4MTZaMBUwEwICEAIXDTIzMDExMjEzMDgxNlqgbjBs +MB8GA1UdIwQYMBaAFExwhjsVUom6tQ+Sqq6xMUETvnPzMDwGA1UdHAQ1MDOgLqAs +hipodHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW2EAf8w +CwYDVR0UBAQCAhABMA0GCSqGSIb3DQEBCwUAA4ICAQCPadbaehEqLv4pwqF8em8T +CW8TOQ4Vjz02uiVk9Bo0za1dQqQmwCBA6UE1BcOh+aWzQxBRz56NeUcfhgDxTntG +xLs896N9MHIG6UxpqJH8cH+DXKHsQjvvCjXtiObmBQR1RiG5C1vEMkfzTt/WSrq5 +7blowLDs4NP6YbtqXEyyUkF7DQSUEUuIDWPQdx1f++nSpVaHWW4xpoO4umesaJco +FuxaXQnZpTHHQfqUJVIL2Mmzvez9thgfKTV3vgkYrGiSLW2m2+Tfga30pUc0qaVI +RrBVORVbcu9m1sV0aJyk96b2T/+i2FRR/np4TOcLgckBpHKeK2FH69lHFr0W/71w +CErNTxahoh82Yi8POenu+S1m2sDnrF1FMf+ZG/i2wr0nW6/+zVGQsEOw77Spbmei +dbEchu3iWF1XEO/n4zVBzl6a1o2RyVg+1pItYd5C5bPwcrfZnBrm4WECPxO+6rbW +2/wz9Iku4XznTLqLEpXLAtenAdo73mLGC7riviX7mhcxfN2UjNfLuVGHmG8XwIsM +Lgpr6DKaxHwpHgW3wA3SGJrY5dj0TvGWaoInrNt1cOMnIpoxRNy5+ko71Ubx3yrV +RhbUMggd1GG1ct9uZn82v74RYF6J8Xcxn9vDFJu5LLT5kvfy414kdJeTXKqfKXA/ +atdUgFa0otoccn5FzyUuzg== +-----END X509 CRL----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate.crl.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate.crl.pem new file mode 100644 index 000000000..a119cede2 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/intermediate.crl.pem @@ -0,0 +1,20 @@ +-----BEGIN X509 CRL----- +MIIDPDCCASQCAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0UxEjAQBgNV +BAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQLDBBNeUlu +dGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBFw0yMjA3MjAy +MDIzNTNaFw0zMjEwMjUyMDIzNTNaMBUwEwICEAIXDTIyMDYxMzEyNDIwNVqgbjBs +MB8GA1UdIwQYMBaAFCuv1TkzC1fSgTfzE1m1u5pRCJsVMDwGA1UdHAQ1MDOgLqAs +hipodHRwOi8vbG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW2EAf8w +CwYDVR0UBAQCAhADMA0GCSqGSIb3DQEBCwUAA4ICAQBbWdqRFsIrG6coL6ln1RL+ +uhgW9l3XMmjNlyiYHHNzOgnlBok9xu9UdaVCOKC6GEthWSzSlBY1AZugje57DQQd +RkIJol9am94lKMTjF/qhzFLiSjho8fwZGDGyES5YeZXkLqNMOf6m/drKaI3iofWf +l63qU9jY8dnSrVDkwgCguUL2FTx60v5H9NPxSctQ3VDxDvDj0sTAcHFknQcZbfvY +ZWpOYNS0FAJlQPVK9wUoDxI0LhrWDq5h/T1jcGO34fPT8RUA5HRtFVUevqSuOLWx +WTfTx5oDeMZPJTvHWUcg4yMElHty4tEvtkFxLSYbZqj7qTU+mi/LAN3UKBH/gBEN +y2OsJvFhVRgHf+zPYegf3WzBSoeaXNAJZ4UnRo34P9AL3Mrh+4OOUP++oYRKjWno +pYtAmTrIwEYoLyisEhhZ6aD92f/Op3dIYsxwhHt0n0lKrbTmUfiJUAe7kUZ4PMn4 +Gg/OHlbEDaDxW1dCymjyRGl+3/8kjy7bkYUXCf7w6JBeL2Hw2dFp1Gh13NRjre93 +PYlSOvI6QNisYGscfuYPwefXogVrNjf/ttCksMa51tUk+ylw7ZMZqQjcPPSzmwKc +5CqpnQHfolvRuN0xIVZiAn5V6/MdHm7ocrXxOkzWQyaoNODTq4js8h8eYXgAkt1w +p1PTEFBucGud7uBDE6Ub6A== +-----END X509 CRL----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/server.cert.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/server.cert.pem new file mode 100644 index 000000000..38cc63534 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/server.cert.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux +EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL +DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X +DTIzMDExMjEzMDgxNloXDTMzMDQxOTEzMDgxNloweDELMAkGA1UEBhMCU0UxEjAQ +BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN +eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEjAQBgNVBAMMCWxv +Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKdU9FaA/n0Z +TXkd10XA9l+UV9xKR65ZTy2ApCFlw2gGWLiUh96a6hX+GQZFUV7ECIDDf+7nC85o +xo1Xyf0rHGABQ0uHlhqSemc12F9APIzRLlQkhtV4vMBBbGQFekje4F9bhY9JQtGd +XJGmwsR+XWo6SUY7K5l9FuSSSRXC0kSYYQfSTPR/LrF6efdHf+ZN4huP7lM2qIFd +afX+qBOI1/Y2LtITo2TaU/hXyKh9wEiuynoq0RZ2KkYQll5cKD9fSD+pW3Xm0XWX +TQy4RZEe3WoYEQsklNw3NC92ocA/PQB9BGNO1fKhzDn6kW2HxDxruDKOuO/meGek +ApCayu3e/I0CAwEAAaOCAagwggGkMAkGA1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQD +AgZAMDMGCWCGSAGG+EIBDQQmFiRPcGVuU1NMIEdlbmVyYXRlZCBTZXJ2ZXIgQ2Vy +dGlmaWNhdGUwHQYDVR0OBBYEFGy5LQPzIelruJl7mL0mtUXM57XhMIGaBgNVHSME +gZIwgY+AFExwhjsVUom6tQ+Sqq6xMUETvnPzoXOkcTBvMQswCQYDVQQGEwJTRTES +MBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlTdG9ja2hvbG0xEjAQBgNVBAoM +CU15T3JnTmFtZTERMA8GA1UECwwITXlSb290Q0ExETAPBgNVBAMMCE15Um9vdENB +ggIQADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwOwYDVR0f +BDQwMjAwoC6gLIYqaHR0cDovL2xvY2FsaG9zdDo5ODc4L2ludGVybWVkaWF0ZS5j +cmwucGVtMDEGCCsGAQUFBwEBBCUwIzAhBggrBgEFBQcwAYYVaHR0cDovL2xvY2Fs +aG9zdDo5ODc3MA0GCSqGSIb3DQEBCwUAA4ICAQCX3EQgiCVqLhnCNd0pmptxXPxo +l1KyZkpdrFa/NgSqRhkuZSAkszwBDDS/gzkHFKEUhmqs6/UZwN4+Rr3LzrHonBiN +aQ6GeNNXZ/3xAQfUCwjjGmz9Sgw6kaX19Gnk2CjI6xP7T+O5UmsMI9hHUepC9nWa +XX2a0hsO/KOVu5ZZckI16Ek/jxs2/HEN0epYdvjKFAaVmzZZ5PATNjrPQXvPmq2r +x++La+3bXZsrH8P2FhPpM5t/IxKKW/Tlpgz92c2jVSIHF5khSA/MFDC+dk80OFmm +v4ZTPIMuZ//Q+wo0f9P48rsL9D27qS7CA+8pn9wu+cfnBDSt7JD5Yipa1gHz71fy +YTa9qRxIAPpzW2v7TFZE8eSKFUY9ipCeM2BbdmCQGmq4+v36b5TZoyjH4k0UVWGo +Gclos2cic5Vxi8E6hb7b7yZpjEfn/5lbCiGMfAnI6aoOyrWg6keaRA33kaLUEZiK +OgFNbPkjiTV0ZQyLXf7uK9YFhpVzJ0dv0CFNse8rZb7A7PLn8VrV/ZFnJ9rPoawn +t7ZGxC0d5BRSEyEeEgsQdxuY4m8OkE18zwhCkt2Qs3uosOWlIrYmqSEa0i/sPSQP +jiwB4nEdBrf8ZygzuYjT5T9YRSwhVox4spS/Av8Ells5JnkuKAhCVv9gHxYwbj0c +CzyLJgE1z9Tq63m+gQ== +-----END CERTIFICATE----- diff --git a/apps/emqx/test/emqx_crl_cache_SUITE_data/server.key.pem b/apps/emqx/test/emqx_crl_cache_SUITE_data/server.key.pem new file mode 100644 index 000000000..d456ece72 --- /dev/null +++ b/apps/emqx/test/emqx_crl_cache_SUITE_data/server.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCnVPRWgP59GU15 +HddFwPZflFfcSkeuWU8tgKQhZcNoBli4lIfemuoV/hkGRVFexAiAw3/u5wvOaMaN +V8n9KxxgAUNLh5YaknpnNdhfQDyM0S5UJIbVeLzAQWxkBXpI3uBfW4WPSULRnVyR +psLEfl1qOklGOyuZfRbkkkkVwtJEmGEH0kz0fy6xenn3R3/mTeIbj+5TNqiBXWn1 +/qgTiNf2Ni7SE6Nk2lP4V8iofcBIrsp6KtEWdipGEJZeXCg/X0g/qVt15tF1l00M +uEWRHt1qGBELJJTcNzQvdqHAPz0AfQRjTtXyocw5+pFth8Q8a7gyjrjv5nhnpAKQ +msrt3vyNAgMBAAECggEABnWvIQ/Fw0qQxRYz00uJt1LguW5cqgxklBsdOvTUwFVO +Y4HIZP2R/9tZV/ahF4l10pK5g52DxSoiUB6Ne6qIY+RolqfbUZdKBmX7vmGadM02 +fqUSV3dbwghEiO/1Mo74FnZQB6IKZFEw26aWakN+k7VAUufB3SEJGzXSgHaO63ru +dFGSiYI8U+q+YnhUJjCnmI12fycNfy451TdUQtGZb6pNmm5HRUF6hpAV8Le9LojP +Ql9eacPpsrzU15X5ElCQZ/f9iNh1bplcISuhrULgKUKOvAVrBlEK67uRVy6g98xA +c/rgNLkbL/jZEsAc3/vHAyFgd3lABfwpBGLHej3QgQKBgQDFNYmfBNQr89HC5Zc+ +M6jXcAT/R+0GNczBTfC4iyNemwqsumSSRelNZ748UefKuS3F6Mvb2CBqE2LbB61G +hrnCffG2pARjZ491SefRwghhWWVGLP1p8KliLgOGBehA1REgJb+XULncjuHZuh4O +LVn3HVnWGxeBGg+yKa6Z4YQi3QKBgQDZN0O8ZcZY74lRJ0UjscD9mJ1yHlsssZag +njkX/f0GR/iVpfaIxQNC3gvWUy2LsU0He9sidcB0cfej0j/qZObQyFsCB0+utOgy ++hX7gokV2pes27WICbNWE2lJL4QZRJgvf82OaEy57kfDrm+eK1XaSZTZ10P82C9u +gAmMnontcQKBgGu29lhY9tqa7jOZ26Yp6Uri8JfO3XPK5u+edqEVvlfqL0Zw+IW8 +kdWpmIqx4f0kcA/tO4v03J+TvycLZmVjKQtGZ0PvCkaRRhY2K9yyMomZnmtaH4BB +5wKtR1do2pauyg/ZDnDDswD5OfsGYWw08TK8YVlEqu3lIjWZ9rguKVIxAoGAZYUk +zVqr10ks3pcCA2rCjkPT4lA5wKvHgI4ylPoKVfMxRY/pp4acvZXV5ne9o7pcDBFh +G7v5FPNnEFPlt4EtN4tMragJH9hBZgHoYEJkG6islweg0lHmVWaBIMlqbfzXO+v5 +gINSyNuLAvP2CvCqEXmubhnkFrpbgMOqsuQuBqECgYB3ss2PDhBF+5qoWgqymFof +1ovRPuQ9sPjWBn5IrCdoYITDnbBzBZERx7GLs6A/PUlWgST7jkb1PY/TxYSUfXzJ +SNd47q0mCQ+IUdqUbHgpK9b1ncwLMsnexpYZdHJWRLgnUhOx7OMjJc/4iLCAFCoN +3KJ7/V1keo7GBHOwnsFcCA== +-----END PRIVATE KEY----- diff --git a/apps/emqx/test/emqx_listeners_SUITE.erl b/apps/emqx/test/emqx_listeners_SUITE.erl index 015439587..107f3d4e7 100644 --- a/apps/emqx/test/emqx_listeners_SUITE.erl +++ b/apps/emqx/test/emqx_listeners_SUITE.erl @@ -26,6 +26,8 @@ -define(CERTS_PATH(CertName), filename:join(["../../lib/emqx/etc/certs/", CertName])). +-define(SERVER_KEY_PASSWORD, "sErve7r8Key$!"). + all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> @@ -33,6 +35,7 @@ init_per_suite(Config) -> application:ensure_all_started(esockd), application:ensure_all_started(quicer), application:ensure_all_started(cowboy), + generate_tls_certs(Config), lists:foreach(fun set_app_env/1, NewConfig), Config. @@ -45,11 +48,6 @@ init_per_testcase(Case, Config) when -> catch emqx_config_handler:stop(), {ok, _} = emqx_config_handler:start_link(), - case emqx_config:get([listeners], undefined) of - undefined -> ok; - Listeners -> emqx_config:put([listeners], maps:remove(quic, Listeners)) - end, - PrevListeners = emqx_config:get([listeners], #{}), PureListeners = remove_default_limiter(PrevListeners), PureListeners2 = PureListeners#{ @@ -185,6 +183,50 @@ t_wss_conn(_) -> {ok, Socket} = ssl:connect({127, 0, 0, 1}, 9998, [{verify, verify_none}], 1000), ok = ssl:close(Socket). +t_quic_conn(Config) -> + Port = 24568, + DataDir = ?config(data_dir, Config), + SSLOpts = #{ + password => ?SERVER_KEY_PASSWORD, + certfile => filename:join(DataDir, "server-password.pem"), + cacertfile => filename:join(DataDir, "ca.pem"), + keyfile => filename:join(DataDir, "server-password.key") + }, + emqx_common_test_helpers:ensure_quic_listener(?FUNCTION_NAME, Port, #{ssl_options => SSLOpts}), + ct:pal("~p", [emqx_listeners:list()]), + {ok, Conn} = quicer:connect( + {127, 0, 0, 1}, + Port, + [ + {verify, verify_none}, + {alpn, ["mqtt"]} + ], + 1000 + ), + ok = quicer:close_connection(Conn), + emqx_listeners:stop_listener(quic, ?FUNCTION_NAME, #{bind => Port}). + +t_ssl_password_cert(Config) -> + Port = 24568, + DataDir = ?config(data_dir, Config), + SSLOptsPWD = #{ + password => ?SERVER_KEY_PASSWORD, + certfile => filename:join(DataDir, "server-password.pem"), + cacertfile => filename:join(DataDir, "ca.pem"), + keyfile => filename:join(DataDir, "server-password.key") + }, + LConf = #{ + enabled => true, + bind => {{127, 0, 0, 1}, Port}, + mountpoint => <<>>, + zone => default, + ssl_options => SSLOptsPWD + }, + ok = emqx_listeners:start_listener(ssl, ?FUNCTION_NAME, LConf), + {ok, SSLSocket} = ssl:connect("127.0.0.1", Port, [{verify, verify_none}]), + ssl:close(SSLSocket), + emqx_listeners:stop_listener(ssl, ?FUNCTION_NAME, LConf). + t_format_bind(_) -> ?assertEqual( ":1883", @@ -269,3 +311,10 @@ remove_default_limiter(Listeners) -> end, Listeners ). + +generate_tls_certs(Config) -> + DataDir = ?config(data_dir, Config), + emqx_common_test_helpers:gen_ca(DataDir, "ca"), + emqx_common_test_helpers:gen_host_cert("server-password", "ca", DataDir, #{ + password => ?SERVER_KEY_PASSWORD + }). diff --git a/apps/emqx/test/emqx_ocsp_cache_SUITE.erl b/apps/emqx/test/emqx_ocsp_cache_SUITE.erl index 235734e9f..3c3fd0341 100644 --- a/apps/emqx/test/emqx_ocsp_cache_SUITE.erl +++ b/apps/emqx/test/emqx_ocsp_cache_SUITE.erl @@ -76,7 +76,7 @@ init_per_testcase(t_openssl_client, Config) -> [], Handler, #{ - extra_mustache_vars => [{test_data_dir, DataDir}], + extra_mustache_vars => #{test_data_dir => DataDir}, conf_file_path => ConfFilePath } ), diff --git a/apps/emqx/test/emqx_quic_multistreams_SUITE.erl b/apps/emqx/test/emqx_quic_multistreams_SUITE.erl index 642b5468c..4afd965bd 100644 --- a/apps/emqx/test/emqx_quic_multistreams_SUITE.erl +++ b/apps/emqx/test/emqx_quic_multistreams_SUITE.erl @@ -1569,7 +1569,7 @@ t_multi_streams_remote_shutdown(Config) -> ok = stop_emqx(), %% Client should be closed - assert_client_die(C). + assert_client_die(C, 100, 50). t_multi_streams_remote_shutdown_with_reconnect(Config) -> erlang:process_flag(trap_exit, true), @@ -2047,14 +2047,15 @@ via_stream({quic, _Conn, Stream}) -> assert_client_die(C) -> assert_client_die(C, 100, 10). assert_client_die(C, _, 0) -> - ct:fail("Client ~p did not die", [C]); + ct:fail("Client ~p did not die: stacktrace: ~p", [C, process_info(C, current_stacktrace)]); assert_client_die(C, Delay, Retries) -> - case catch emqtt:info(C) of - {'EXIT', {noproc, {gen_statem, call, [_, info, infinity]}}} -> - ok; - _Other -> + try emqtt:info(C) of + Info when is_list(Info) -> timer:sleep(Delay), assert_client_die(C, Delay, Retries - 1) + catch + exit:Error -> + ct:comment("client die with ~p", [Error]) end. %% BUILD_WITHOUT_QUIC diff --git a/apps/emqx_authz/etc/acl.conf b/apps/emqx_authz/etc/acl.conf index d39490d46..a64287a4a 100644 --- a/apps/emqx_authz/etc/acl.conf +++ b/apps/emqx_authz/etc/acl.conf @@ -23,7 +23,7 @@ %% -type(rule() :: {permission(), who(), access(), topics()} | {permission(), all}). %%-------------------------------------------------------------------- -{allow, {username, "^dashboard?"}, subscribe, ["$SYS/#"]}. +{allow, {username, {re, "^dashboard$"}}, subscribe, ["$SYS/#"]}. {allow, {ipaddr, "127.0.0.1"}, all, ["$SYS/#", "#"]}. diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index b3ce04f43..84b1d903e 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -26,6 +26,8 @@ -include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). +-import(emqx_common_test_helpers, [on_exit/1]). + all() -> emqx_common_test_helpers:all(?MODULE). @@ -65,6 +67,7 @@ end_per_suite(_Config) -> init_per_testcase(TestCase, Config) when TestCase =:= t_subscribe_deny_disconnect_publishes_last_will_testament; + TestCase =:= t_publish_last_will_testament_banned_client_connecting; TestCase =:= t_publish_deny_disconnect_publishes_last_will_testament -> {ok, _} = emqx_authz:update(?CMD_REPLACE, []), @@ -76,11 +79,15 @@ init_per_testcase(_, Config) -> end_per_testcase(TestCase, _Config) when TestCase =:= t_subscribe_deny_disconnect_publishes_last_will_testament; + TestCase =:= t_publish_last_will_testament_banned_client_connecting; TestCase =:= t_publish_deny_disconnect_publishes_last_will_testament -> {ok, _} = emqx:update_config([authorization, deny_action], ignore), + {ok, _} = emqx_authz:update(?CMD_REPLACE, []), + emqx_common_test_helpers:call_janitor(), ok; end_per_testcase(_TestCase, _Config) -> + emqx_common_test_helpers:call_janitor(), ok. set_special_configs(emqx_authz) -> @@ -396,5 +403,63 @@ t_publish_last_will_testament_denied_topic(_Config) -> ok. +%% client is allowed by ACL to publish to its LWT topic, is connected, +%% and then gets banned and kicked out while connected. Should not +%% publish LWT. +t_publish_last_will_testament_banned_client_connecting(_Config) -> + {ok, _} = emqx_authz:update(?CMD_REPLACE, [?SOURCE7]), + Username = <<"some_client">>, + ClientId = <<"some_clientid">>, + LWTPayload = <<"should not be published">>, + LWTTopic = <<"some_client/lwt">>, + ok = emqx:subscribe(<<"some_client/lwt">>), + {ok, C} = emqtt:start_link([ + {clientid, ClientId}, + {username, Username}, + {will_topic, LWTTopic}, + {will_payload, LWTPayload} + ]), + ?assertMatch({ok, _}, emqtt:connect(C)), + + %% Now we ban the client while it is connected. + Now = erlang:system_time(second), + Who = {username, Username}, + emqx_banned:create(#{ + who => Who, + by => <<"test">>, + reason => <<"test">>, + at => Now, + until => Now + 120 + }), + on_exit(fun() -> emqx_banned:delete(Who) end), + %% Now kick it as we do in the ban API. + process_flag(trap_exit, true), + ?check_trace( + begin + ok = emqx_cm:kick_session(ClientId), + receive + {deliver, LWTTopic, #message{payload = LWTPayload}} -> + error(lwt_should_not_be_published_to_forbidden_topic) + after 2_000 -> ok + end, + ok + end, + fun(Trace) -> + ?assertMatch( + [ + #{ + client_banned := true, + publishing_disallowed := false + } + ], + ?of_kind(last_will_testament_publish_denied, Trace) + ), + ok + end + ), + ok = snabbkaffe:stop(), + + ok. + stop_apps(Apps) -> lists:foreach(fun application:stop/1, Apps). diff --git a/apps/emqx_bridge/src/emqx_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl index 98ce6a8b0..bf91d20f7 100644 --- a/apps/emqx_bridge/src/emqx_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -67,7 +67,9 @@ T == timescale; T == matrix; T == tdengine; - T == dynamo + T == dynamo; + T == rocketmq; + T == cassandra ). load() -> diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index b9a6d4c06..586c66bef 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -20,6 +20,7 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("hocon/include/hoconsc.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("emqx/include/emqx_api_lib.hrl"). -include_lib("emqx_bridge/include/emqx_bridge.hrl"). -import(hoconsc, [mk/2, array/1, enum/1]). @@ -46,18 +47,14 @@ -export([lookup_from_local_node/2]). --define(BAD_REQUEST(Reason), {400, error_msg('BAD_REQUEST', Reason)}). - -define(BRIDGE_NOT_ENABLED, ?BAD_REQUEST(<<"Forbidden operation, bridge not enabled">>) ). --define(NOT_FOUND(Reason), {404, error_msg('NOT_FOUND', Reason)}). - --define(BRIDGE_NOT_FOUND(BridgeType, BridgeName), +-define(BRIDGE_NOT_FOUND(BRIDGE_TYPE, BRIDGE_NAME), ?NOT_FOUND( - <<"Bridge lookup failed: bridge named '", (BridgeName)/binary, "' of type ", - (bin(BridgeType))/binary, " does not exist.">> + <<"Bridge lookup failed: bridge named '", (BRIDGE_NAME)/binary, "' of type ", + (bin(BRIDGE_TYPE))/binary, " does not exist.">> ) ). @@ -221,7 +218,7 @@ info_example_basic(webhook) -> health_check_interval => 15000, auto_restart_interval => 15000, query_mode => async, - async_inflight_window => 100, + inflight_window => 100, max_queue_bytes => 100 * 1024 * 1024 } }; @@ -284,7 +281,7 @@ schema("/bridges") -> 'operationId' => '/bridges', get => #{ tags => [<<"bridges">>], - summary => <<"List Bridges">>, + summary => <<"List bridges">>, description => ?DESC("desc_api1"), responses => #{ 200 => emqx_dashboard_swagger:schema_with_example( @@ -295,7 +292,7 @@ schema("/bridges") -> }, post => #{ tags => [<<"bridges">>], - summary => <<"Create Bridge">>, + summary => <<"Create bridge">>, description => ?DESC("desc_api2"), 'requestBody' => emqx_dashboard_swagger:schema_with_examples( emqx_bridge_schema:post_request(), @@ -312,7 +309,7 @@ schema("/bridges/:id") -> 'operationId' => '/bridges/:id', get => #{ tags => [<<"bridges">>], - summary => <<"Get Bridge">>, + summary => <<"Get bridge">>, description => ?DESC("desc_api3"), parameters => [param_path_id()], responses => #{ @@ -322,7 +319,7 @@ schema("/bridges/:id") -> }, put => #{ tags => [<<"bridges">>], - summary => <<"Update Bridge">>, + summary => <<"Update bridge">>, description => ?DESC("desc_api4"), parameters => [param_path_id()], 'requestBody' => emqx_dashboard_swagger:schema_with_examples( @@ -337,7 +334,7 @@ schema("/bridges/:id") -> }, delete => #{ tags => [<<"bridges">>], - summary => <<"Delete Bridge">>, + summary => <<"Delete bridge">>, description => ?DESC("desc_api5"), parameters => [param_path_id()], responses => #{ @@ -356,7 +353,7 @@ schema("/bridges/:id/metrics") -> 'operationId' => '/bridges/:id/metrics', get => #{ tags => [<<"bridges">>], - summary => <<"Get Bridge Metrics">>, + summary => <<"Get bridge metrics">>, description => ?DESC("desc_bridge_metrics"), parameters => [param_path_id()], responses => #{ @@ -370,7 +367,7 @@ schema("/bridges/:id/metrics/reset") -> 'operationId' => '/bridges/:id/metrics/reset', put => #{ tags => [<<"bridges">>], - summary => <<"Reset Bridge Metrics">>, + summary => <<"Reset bridge metrics">>, description => ?DESC("desc_api6"), parameters => [param_path_id()], responses => #{ @@ -385,7 +382,7 @@ schema("/bridges/:id/enable/:enable") -> put => #{ tags => [<<"bridges">>], - summary => <<"Enable or Disable Bridge">>, + summary => <<"Enable or disable bridge">>, desc => ?DESC("desc_enable_bridge"), parameters => [param_path_id(), param_path_enable()], responses => @@ -401,7 +398,7 @@ schema("/bridges/:id/:operation") -> 'operationId' => '/bridges/:id/:operation', post => #{ tags => [<<"bridges">>], - summary => <<"Stop or Restart Bridge">>, + summary => <<"Stop or restart bridge">>, description => ?DESC("desc_api7"), parameters => [ param_path_id(), @@ -423,7 +420,7 @@ schema("/nodes/:node/bridges/:id/:operation") -> 'operationId' => '/nodes/:node/bridges/:id/:operation', post => #{ tags => [<<"bridges">>], - summary => <<"Stop/Restart Bridge">>, + summary => <<"Stop/restart bridge">>, description => ?DESC("desc_api8"), parameters => [ param_path_node(), @@ -463,11 +460,10 @@ schema("/bridges_probe") -> '/bridges'(post, #{body := #{<<"type">> := BridgeType, <<"name">> := BridgeName} = Conf0}) -> case emqx_bridge:lookup(BridgeType, BridgeName) of {ok, _} -> - {400, error_msg('ALREADY_EXISTS', <<"bridge already exists">>)}; + ?BAD_REQUEST('ALREADY_EXISTS', <<"bridge already exists">>); {error, not_found} -> Conf = filter_out_request_body(Conf0), - {ok, _} = emqx_bridge:create(BridgeType, BridgeName, Conf), - lookup_from_all_nodes(BridgeType, BridgeName, 201) + create_bridge(BridgeType, BridgeName, Conf) end; '/bridges'(get, _Params) -> Nodes = mria:running_nodes(), @@ -478,9 +474,9 @@ schema("/bridges_probe") -> [format_resource(Data, Node) || Data <- Bridges] || {Node, Bridges} <- lists:zip(Nodes, NodeBridges) ], - {200, zip_bridges(AllBridges)}; + ?OK(zip_bridges(AllBridges)); {error, Reason} -> - {500, error_msg('INTERNAL_ERROR', Reason)} + ?INTERNAL_ERROR(Reason) end. '/bridges/:id'(get, #{bindings := #{id := Id}}) -> @@ -493,8 +489,7 @@ schema("/bridges_probe") -> {ok, _} -> RawConf = emqx:get_raw_config([bridges, BridgeType, BridgeName], #{}), Conf = deobfuscate(Conf1, RawConf), - {ok, _} = emqx_bridge:create(BridgeType, BridgeName, Conf), - lookup_from_all_nodes(BridgeType, BridgeName, 200); + update_bridge(BridgeType, BridgeName, Conf); {error, not_found} -> ?BRIDGE_NOT_FOUND(BridgeType, BridgeName) end @@ -512,16 +507,16 @@ schema("/bridges_probe") -> end, case emqx_bridge:check_deps_and_remove(BridgeType, BridgeName, AlsoDeleteActs) of {ok, _} -> - 204; + ?NO_CONTENT; {error, {rules_deps_on_this_bridge, RuleIds}} -> ?BAD_REQUEST( {<<"Cannot delete bridge while active rules are defined for this bridge">>, RuleIds} ); {error, timeout} -> - {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; + ?SERVICE_UNAVAILABLE(<<"request timeout">>); {error, Reason} -> - {500, error_msg('INTERNAL_ERROR', Reason)} + ?INTERNAL_ERROR(Reason) end; {error, not_found} -> ?BRIDGE_NOT_FOUND(BridgeType, BridgeName) @@ -538,7 +533,7 @@ schema("/bridges_probe") -> ok = emqx_bridge_resource:reset_metrics( emqx_bridge_resource:resource_id(BridgeType, BridgeName) ), - {204} + ?NO_CONTENT end ). @@ -549,9 +544,9 @@ schema("/bridges_probe") -> Params1 = maybe_deobfuscate_bridge_probe(Params), case emqx_bridge_resource:create_dry_run(ConnType, maps:remove(<<"type">>, Params1)) of ok -> - 204; + ?NO_CONTENT; {error, Reason} when not is_tuple(Reason); element(1, Reason) =/= 'exit' -> - {400, error_msg('TEST_FAILED', to_hr_reason(Reason))} + ?BAD_REQUEST('TEST_FAILED', Reason) end; BadRequest -> BadRequest @@ -585,7 +580,7 @@ do_lookup_from_all_nodes(BridgeType, BridgeName, SuccCode, FormatFun) -> {ok, [{error, not_found} | _]} -> ?BRIDGE_NOT_FOUND(BridgeType, BridgeName); {error, Reason} -> - {500, error_msg('INTERNAL_ERROR', Reason)} + ?INTERNAL_ERROR(Reason) end. lookup_from_local_node(BridgeType, BridgeName) -> @@ -594,6 +589,20 @@ lookup_from_local_node(BridgeType, BridgeName) -> Error -> Error end. +create_bridge(BridgeType, BridgeName, Conf) -> + create_or_update_bridge(BridgeType, BridgeName, Conf, 201). + +update_bridge(BridgeType, BridgeName, Conf) -> + create_or_update_bridge(BridgeType, BridgeName, Conf, 200). + +create_or_update_bridge(BridgeType, BridgeName, Conf, HttpStatusCode) -> + case emqx_bridge:create(BridgeType, BridgeName, Conf) of + {ok, _} -> + lookup_from_all_nodes(BridgeType, BridgeName, HttpStatusCode); + {error, #{kind := validation_error} = Reason} -> + ?BAD_REQUEST(map_to_json(Reason)) + end. + '/bridges/:id/enable/:enable'(put, #{bindings := #{id := Id, enable := Enable}}) -> ?TRY_PARSE_ID( Id, @@ -603,15 +612,15 @@ lookup_from_local_node(BridgeType, BridgeName) -> OperFunc -> case emqx_bridge:disable_enable(OperFunc, BridgeType, BridgeName) of {ok, _} -> - 204; + ?NO_CONTENT; {error, {pre_config_update, _, bridge_not_found}} -> ?BRIDGE_NOT_FOUND(BridgeType, BridgeName); {error, {_, _, timeout}} -> - {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; + ?SERVICE_UNAVAILABLE(<<"request timeout">>); {error, timeout} -> - {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; + ?SERVICE_UNAVAILABLE(<<"request timeout">>); {error, Reason} -> - {500, error_msg('INTERNAL_ERROR', Reason)} + ?INTERNAL_ERROR(Reason) end end ). @@ -731,7 +740,7 @@ pick_bridges_by_id(Type, Name, BridgesAllNodes) -> format_bridge_info([FirstBridge | _] = Bridges) -> Res = maps:without([node, metrics], FirstBridge), - NodeStatus = collect_status(Bridges), + NodeStatus = node_status(Bridges), redact(Res#{ status => aggregate_status(NodeStatus), node_status => NodeStatus @@ -744,8 +753,8 @@ format_bridge_metrics(Bridges) -> node_metrics => NodeMetrics }. -collect_status(Bridges) -> - [maps:with([node, status], B) || B <- Bridges]. +node_status(Bridges) -> + [maps:with([node, status, status_reason], B) || B <- Bridges]. aggregate_status(AllStatus) -> Head = fun([A | _]) -> A end, @@ -816,52 +825,63 @@ format_resource( ) ). -format_resource_data(#{status := Status, metrics := Metrics}) -> - #{status => Status, metrics => format_metrics(Metrics)}; -format_resource_data(#{status := Status}) -> - #{status => Status}. +format_resource_data(ResData) -> + maps:fold(fun format_resource_data/3, #{}, maps:with([status, metrics, error], ResData)). -format_metrics(#{ - counters := #{ - 'dropped' := Dropped, - 'dropped.other' := DroppedOther, - 'dropped.expired' := DroppedExpired, - 'dropped.queue_full' := DroppedQueueFull, - 'dropped.resource_not_found' := DroppedResourceNotFound, - 'dropped.resource_stopped' := DroppedResourceStopped, - 'matched' := Matched, - 'retried' := Retried, - 'late_reply' := LateReply, - 'failed' := SentFailed, - 'success' := SentSucc, - 'received' := Rcvd +format_resource_data(error, undefined, Result) -> + Result; +format_resource_data(error, Error, Result) -> + Result#{status_reason => emqx_misc:readable_error_msg(Error)}; +format_resource_data( + metrics, + #{ + counters := #{ + 'dropped' := Dropped, + 'dropped.other' := DroppedOther, + 'dropped.expired' := DroppedExpired, + 'dropped.queue_full' := DroppedQueueFull, + 'dropped.resource_not_found' := DroppedResourceNotFound, + 'dropped.resource_stopped' := DroppedResourceStopped, + 'matched' := Matched, + 'retried' := Retried, + 'late_reply' := LateReply, + 'failed' := SentFailed, + 'success' := SentSucc, + 'received' := Rcvd + }, + gauges := Gauges, + rate := #{ + matched := #{current := Rate, last5m := Rate5m, max := RateMax} + } }, - gauges := Gauges, - rate := #{ - matched := #{current := Rate, last5m := Rate5m, max := RateMax} - } -}) -> + Result +) -> Queued = maps:get('queuing', Gauges, 0), SentInflight = maps:get('inflight', Gauges, 0), - ?METRICS( - Dropped, - DroppedOther, - DroppedExpired, - DroppedQueueFull, - DroppedResourceNotFound, - DroppedResourceStopped, - Matched, - Queued, - Retried, - LateReply, - SentFailed, - SentInflight, - SentSucc, - Rate, - Rate5m, - RateMax, - Rcvd - ). + Result#{ + metrics => + ?METRICS( + Dropped, + DroppedOther, + DroppedExpired, + DroppedQueueFull, + DroppedResourceNotFound, + DroppedResourceStopped, + Matched, + Queued, + Retried, + LateReply, + SentFailed, + SentInflight, + SentSucc, + Rate, + Rate5m, + RateMax, + Rcvd + ) + }; +format_resource_data(K, V, Result) -> + Result#{K => V}. fill_defaults(Type, RawConf) -> PackedConf = pack_bridge_conf(Type, RawConf), @@ -903,6 +923,7 @@ filter_out_request_body(Conf) -> <<"type">>, <<"name">>, <<"status">>, + <<"status_reason">>, <<"node_status">>, <<"node_metrics">>, <<"metrics">>, @@ -910,9 +931,6 @@ filter_out_request_body(Conf) -> ], maps:without(ExtraConfs, Conf). -error_msg(Code, Msg) -> - #{code => Code, message => emqx_misc:readable_error_msg(Msg)}. - bin(S) when is_list(S) -> list_to_binary(S); bin(S) when is_atom(S) -> @@ -923,30 +941,31 @@ bin(S) when is_binary(S) -> call_operation(NodeOrAll, OperFunc, Args = [_Nodes, BridgeType, BridgeName]) -> case is_ok(do_bpapi_call(NodeOrAll, OperFunc, Args)) of Ok when Ok =:= ok; is_tuple(Ok), element(1, Ok) =:= ok -> - 204; + ?NO_CONTENT; {error, not_implemented} -> %% Should only happen if we call `start` on a node that is %% still on an older bpapi version that doesn't support it. maybe_try_restart(NodeOrAll, OperFunc, Args); {error, timeout} -> - {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; + ?SERVICE_UNAVAILABLE(<<"Request timeout">>); {error, {start_pool_failed, Name, Reason}} -> - {503, - error_msg( - 'SERVICE_UNAVAILABLE', - bin( - io_lib:format( - "failed to start ~p pool for reason ~p", - [Name, Reason] - ) - ) - )}; + ?SERVICE_UNAVAILABLE( + bin(io_lib:format("Failed to start ~p pool for reason ~p", [Name, Reason])) + ); {error, not_found} -> - ?BRIDGE_NOT_FOUND(BridgeType, BridgeName); + BridgeId = emqx_bridge_resource:bridge_id(BridgeType, BridgeName), + ?SLOG(warning, #{ + msg => "bridge_inconsistent_in_cluster_for_call_operation", + reason => not_found, + type => BridgeType, + name => BridgeName, + bridge => BridgeId + }), + ?SERVICE_UNAVAILABLE(<<"Bridge not found on remote node: ", BridgeId/binary>>); {error, {node_not_found, Node}} -> ?NOT_FOUND(<<"Node not found: ", (atom_to_binary(Node))/binary>>); {error, Reason} when not is_tuple(Reason); element(1, Reason) =/= 'exit' -> - ?BAD_REQUEST(to_hr_reason(Reason)) + ?BAD_REQUEST(Reason) end. maybe_try_restart(all, start_bridges_to_all_nodes, Args) -> @@ -954,7 +973,7 @@ maybe_try_restart(all, start_bridges_to_all_nodes, Args) -> maybe_try_restart(Node, start_bridge_to_node, Args) -> call_operation(Node, restart_bridge_to_node, Args); maybe_try_restart(_, _, _) -> - 501. + ?NOT_IMPLEMENTED. do_bpapi_call(all, Call, Args) -> maybe_unwrap( @@ -985,19 +1004,6 @@ supported_versions(start_bridge_to_node) -> [2, 3]; supported_versions(start_bridges_to_all_nodes) -> [2, 3]; supported_versions(_Call) -> [1, 2, 3]. -to_hr_reason(nxdomain) -> - <<"Host not found">>; -to_hr_reason(econnrefused) -> - <<"Connection refused">>; -to_hr_reason({unauthorized_client, _}) -> - <<"Unauthorized client">>; -to_hr_reason({not_authorized, _}) -> - <<"Not authorized">>; -to_hr_reason({malformed_username_or_password, _}) -> - <<"Malformed username or password">>; -to_hr_reason(Reason) -> - Reason. - redact(Term) -> emqx_misc:redact(Term). @@ -1021,3 +1027,8 @@ deobfuscate(NewConf, OldConf) -> #{}, NewConf ). + +map_to_json(M) -> + emqx_json:encode( + emqx_map_lib:jsonable_map(M, fun(K, V) -> {K, emqx_map_lib:binary_string(V)} end) + ). diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl b/apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl index 1e55d0c0e..fe173fa89 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl @@ -86,7 +86,7 @@ default_ssl() -> default_resource_opts() -> #{ - <<"async_inflight_window">> => 100, + <<"inflight_window">> => 100, <<"auto_restart_interval">> => <<"60s">>, <<"health_check_interval">> => <<"15s">>, <<"max_queue_bytes">> => <<"1GB">>, diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl index 74d2a5ca1..6c278a5ec 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl @@ -106,6 +106,12 @@ common_bridge_fields() -> status_fields() -> [ {"status", mk(status(), #{desc => ?DESC("desc_status")})}, + {"status_reason", + mk(binary(), #{ + required => false, + desc => ?DESC("desc_status_reason"), + example => <<"Connection refused">> + })}, {"node_status", mk( hoconsc:array(ref(?MODULE, "node_status")), @@ -190,7 +196,13 @@ fields("node_metrics") -> fields("node_status") -> [ node_name(), - {"status", mk(status(), #{})} + {"status", mk(status(), #{})}, + {"status_reason", + mk(binary(), #{ + required => false, + desc => ?DESC("desc_status_reason"), + example => <<"Connection refused">> + })} ]. desc(bridges) -> diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index ab24ccc8f..8899cd24a 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -18,12 +18,15 @@ -compile(nowarn_export_all). -compile(export_all). --import(emqx_mgmt_api_test_util, [request/3, uri/1]). +-import(emqx_mgmt_api_test_util, [uri/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"bridges: {}">>). --define(BRIDGE_TYPE, <<"webhook">>). +-include_lib("snabbkaffe/include/test_macros.hrl"). + +-define(SUITE_APPS, [emqx_conf, emqx_authn, emqx_management, emqx_rule_engine, emqx_bridge]). + +-define(BRIDGE_TYPE_HTTP, <<"webhook">>). -define(BRIDGE_NAME, (atom_to_binary(?FUNCTION_NAME))). -define(URL(PORT, PATH), list_to_binary( @@ -48,43 +51,129 @@ }). -define(MQTT_BRIDGE(SERVER), ?MQTT_BRIDGE(SERVER, <<"mqtt_egress_test_bridge">>)). --define(HTTP_BRIDGE(URL, TYPE, NAME), ?BRIDGE(NAME, TYPE)#{ +-define(HTTP_BRIDGE(URL, NAME), ?BRIDGE(NAME, ?BRIDGE_TYPE_HTTP)#{ <<"url">> => URL, <<"local_topic">> => <<"emqx_webhook/#">>, <<"method">> => <<"post">>, <<"body">> => <<"${payload}">>, <<"headers">> => #{ - <<"content-type">> => <<"application/json">> + % NOTE + % The Pascal-Case is important here. + % The reason is kinda ridiculous: `emqx_bridge_resource:create_dry_run/2` converts + % bridge config keys into atoms, and the atom 'Content-Type' exists in the ERTS + % when this happens (while the 'content-type' does not). + <<"Content-Type">> => <<"application/json">> } }). +-define(HTTP_BRIDGE(URL), ?HTTP_BRIDGE(URL, ?BRIDGE_NAME)). all() -> - emqx_common_test_helpers:all(?MODULE). + [ + {group, single}, + {group, cluster} + ]. groups() -> - []. + SingleOnlyTests = [ + t_broken_bpapi_vsn, + t_old_bpapi_vsn, + t_bridges_probe + ], + [ + {single, [], emqx_common_test_helpers:all(?MODULE)}, + {cluster, [], emqx_common_test_helpers:all(?MODULE) -- SingleOnlyTests} + ]. suite() -> [{timetrap, {seconds, 60}}]. init_per_suite(Config) -> - _ = application:load(emqx_conf), - %% some testcases (may from other app) already get emqx_connector started - _ = application:stop(emqx_resource), - _ = application:stop(emqx_connector), - ok = emqx_mgmt_api_test_util:init_suite( - [emqx_rule_engine, emqx_bridge, emqx_authn] - ), - ok = emqx_common_test_helpers:load_config( - emqx_rule_engine_schema, - <<"rule_engine {rules {}}">> - ), - ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?CONF_DEFAULT), Config. end_per_suite(_Config) -> - emqx_mgmt_api_test_util:end_suite([emqx_rule_engine, emqx_bridge, emqx_authn]), - mria:clear_table(emqx_authn_mnesia), + ok. + +init_per_group(cluster, Config) -> + Cluster = mk_cluster_specs(Config), + ct:pal("Starting ~p", [Cluster]), + Nodes = [ + emqx_common_test_helpers:start_slave(Name, Opts) + || {Name, Opts} <- Cluster + ], + [NodePrimary | NodesRest] = Nodes, + ok = erpc:call(NodePrimary, fun() -> init_node(primary) end), + _ = [ok = erpc:call(Node, fun() -> init_node(regular) end) || Node <- NodesRest], + [{group, cluster}, {cluster_nodes, Nodes}, {api_node, NodePrimary} | Config]; +init_per_group(_, Config) -> + ok = emqx_mgmt_api_test_util:init_suite(?SUITE_APPS), + ok = load_suite_config(emqx_rule_engine), + ok = load_suite_config(emqx_bridge), + [{group, single}, {api_node, node()} | Config]. + +mk_cluster_specs(Config) -> + Specs = [ + {core, emqx_bridge_api_SUITE1, #{}}, + {core, emqx_bridge_api_SUITE2, #{}} + ], + CommonOpts = #{ + env => [{emqx, boot_modules, [broker]}], + apps => [], + % NOTE + % We need to start all those apps _after_ the cluster becomes stable, in the + % `init_node/1`. This is because usual order is broken in very subtle way: + % 1. Node starts apps including `mria` and `emqx_conf` which starts `emqx_cluster_rpc`. + % 2. The `emqx_cluster_rpc` sets up a mnesia table subscription during initialization. + % 3. In the meantime `mria` joins the cluster and notices it should restart. + % 4. Mnesia subscription becomes lost during restarts (god knows why). + % Yet we need to load them before, so that mria / mnesia will know which tables + % should be created in the cluster. + % TODO + % We probably should hide these intricacies behind the `emqx_common_test_helpers`. + load_apps => ?SUITE_APPS ++ [emqx_dashboard], + env_handler => fun load_suite_config/1, + load_schema => false, + priv_data_dir => ?config(priv_dir, Config) + }, + emqx_common_test_helpers:emqx_cluster(Specs, CommonOpts). + +init_node(Type) -> + ok = emqx_common_test_helpers:start_apps(?SUITE_APPS, fun load_suite_config/1), + case Type of + primary -> + ok = emqx_config:put( + [dashboard, listeners], + #{http => #{enable => true, bind => 18083}} + ), + ok = emqx_dashboard:start_listeners(), + ready = emqx_dashboard_listener:regenerate_minirest_dispatch(), + emqx_common_test_http:create_default_app(); + regular -> + ok + end. + +load_suite_config(emqx_rule_engine) -> + ok = emqx_common_test_helpers:load_config( + emqx_rule_engine_schema, + <<"rule_engine { rules {} }">> + ); +load_suite_config(emqx_bridge) -> + ok = emqx_common_test_helpers:load_config( + emqx_bridge_schema, + <<"bridges {}">> + ); +load_suite_config(_) -> + ok. + +end_per_group(cluster, Config) -> + ok = lists:foreach( + fun(Node) -> + _ = erpc:call(Node, emqx_common_test_helpers, stop_apps, [?SUITE_APPS]), + emqx_common_test_helpers:stop_slave(Node) + end, + ?config(cluster_nodes, Config) + ); +end_per_group(_, _Config) -> + emqx_mgmt_api_test_util:end_suite(?SUITE_APPS), ok. init_per_testcase(t_broken_bpapi_vsn, Config) -> @@ -98,7 +187,6 @@ init_per_testcase(t_old_bpapi_vsn, Config) -> meck:expect(emqx_bpapi, supported_version, 2, 1), init_per_testcase(common, Config); init_per_testcase(_, Config) -> - {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {Port, Sock, Acceptor} = start_http_server(fun handle_fun_200_ok/2), [{port, Port}, {sock, Sock}, {acceptor, Acceptor} | Config]. @@ -111,8 +199,10 @@ end_per_testcase(t_old_bpapi_vsn, Config) -> end_per_testcase(_, Config) -> Sock = ?config(sock, Config), Acceptor = ?config(acceptor, Config), - stop_http_server(Sock, Acceptor), - clear_resources(), + Node = ?config(api_node, Config), + ok = emqx_common_test_helpers:call_janitor(), + ok = stop_http_server(Sock, Acceptor), + ok = erpc:call(Node, fun clear_resources/0), ok. clear_resources() -> @@ -194,35 +284,36 @@ parse_http_request(ReqStr0) -> t_http_crud_apis(Config) -> Port = ?config(port, Config), %% assert we there's no bridges at first - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), - {ok, 404, _} = request(get, uri(["bridges", "foo"]), []), - {ok, 404, _} = request(get, uri(["bridges", "webhook:foo"]), []), + {ok, 404, _} = request(get, uri(["bridges", "foo"]), Config), + {ok, 404, _} = request(get, uri(["bridges", "webhook:foo"]), Config), %% then we add a webhook bridge, using POST %% POST /bridges/ will create a bridge URL1 = ?URL(Port, "path1"), Name = ?BRIDGE_NAME, - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ?assertMatch( + {ok, 201, #{ + <<"type">> := ?BRIDGE_TYPE_HTTP, + <<"name">> := Name, + <<"enable">> := true, + <<"status">> := _, + <<"node_status">> := [_ | _], + <<"url">> := URL1 + }}, + request_json( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, Name), + Config + ) ), - %ct:pal("---bridge: ~p", [Bridge]), - #{ - <<"type">> := ?BRIDGE_TYPE, - <<"name">> := Name, - <<"enable">> := true, - <<"status">> := _, - <<"node_status">> := [_ | _], - <<"url">> := URL1 - } = emqx_json:decode(Bridge, [return_maps]), - - BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name), %% send an message to emqx and the message should be forwarded to the HTTP server Body = <<"my msg">>, - emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)), + _ = publish_message(<<"emqx_webhook/1">>, Body, Config), ?assert( receive {http_server, received, #{ @@ -240,55 +331,53 @@ t_http_crud_apis(Config) -> ), %% update the request-path of the bridge URL2 = ?URL(Port, "path2"), - {ok, 200, Bridge2} = request( - put, - uri(["bridges", BridgeID]), - ?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, Name) - ), ?assertMatch( - #{ - <<"type">> := ?BRIDGE_TYPE, + {ok, 200, #{ + <<"type">> := ?BRIDGE_TYPE_HTTP, <<"name">> := Name, <<"enable">> := true, <<"status">> := _, <<"node_status">> := [_ | _], <<"url">> := URL2 - }, - emqx_json:decode(Bridge2, [return_maps]) + }}, + request_json( + put, + uri(["bridges", BridgeID]), + ?HTTP_BRIDGE(URL2, Name), + Config + ) ), %% list all bridges again, assert Bridge2 is in it - {ok, 200, Bridge2Str} = request(get, uri(["bridges"]), []), ?assertMatch( - [ + {ok, 200, [ #{ - <<"type">> := ?BRIDGE_TYPE, + <<"type">> := ?BRIDGE_TYPE_HTTP, <<"name">> := Name, <<"enable">> := true, <<"status">> := _, <<"node_status">> := [_ | _], <<"url">> := URL2 } - ], - emqx_json:decode(Bridge2Str, [return_maps]) + ]}, + request_json(get, uri(["bridges"]), Config) ), %% get the bridge by id - {ok, 200, Bridge3Str} = request(get, uri(["bridges", BridgeID]), []), ?assertMatch( - #{ - <<"type">> := ?BRIDGE_TYPE, + {ok, 200, #{ + <<"type">> := ?BRIDGE_TYPE_HTTP, <<"name">> := Name, <<"enable">> := true, <<"status">> := _, <<"node_status">> := [_ | _], <<"url">> := URL2 - }, - emqx_json:decode(Bridge3Str, [return_maps]) + }}, + request_json(get, uri(["bridges", BridgeID]), Config) ), %% send an message to emqx again, check the path has been changed - emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)), + _ = publish_message(<<"emqx_webhook/1">>, Body, Config), ?assert( receive {http_server, received, #{path := <<"/path2">>}} -> @@ -301,49 +390,120 @@ t_http_crud_apis(Config) -> end ), - %% delete the bridge - {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), - - %% update a deleted bridge returns an error - {ok, 404, ErrMsg2} = request( + %% Test bad updates + {ok, 400, PutFail1} = request_json( put, uri(["bridges", BridgeID]), - ?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, Name) + maps:remove(<<"url">>, ?HTTP_BRIDGE(URL2, Name)), + Config + ), + ?assertMatch( + #{<<"reason">> := <<"required_field">>}, + json(maps:get(<<"message">>, PutFail1)) + ), + {ok, 400, PutFail2} = request_json( + put, + uri(["bridges", BridgeID]), + maps:put(<<"curl">>, URL2, maps:remove(<<"url">>, ?HTTP_BRIDGE(URL2, Name))), + Config ), ?assertMatch( #{ + <<"reason">> := <<"unknown_fields">>, + <<"unknown">> := <<"curl">> + }, + json(maps:get(<<"message">>, PutFail2)) + ), + + %% delete the bridge + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), Config), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), + + %% update a deleted bridge returns an error + ?assertMatch( + {ok, 404, #{ <<"code">> := <<"NOT_FOUND">>, <<"message">> := _ - }, - emqx_json:decode(ErrMsg2, [return_maps]) + }}, + request_json( + put, + uri(["bridges", BridgeID]), + ?HTTP_BRIDGE(URL2, Name), + Config + ) ), %% try delete bad bridge id - {ok, 404, BadId} = request(delete, uri(["bridges", "foo"]), []), ?assertMatch( - #{ + {ok, 404, #{ <<"code">> := <<"NOT_FOUND">>, <<"message">> := <<"Invalid bridge ID", _/binary>> - }, - emqx_json:decode(BadId, [return_maps]) + }}, + request_json(delete, uri(["bridges", "foo"]), Config) ), %% Deleting a non-existing bridge should result in an error - {ok, 404, ErrMsg3} = request(delete, uri(["bridges", BridgeID]), []), ?assertMatch( - #{ + {ok, 404, #{ <<"code">> := <<"NOT_FOUND">>, <<"message">> := _ - }, - emqx_json:decode(ErrMsg3, [return_maps]) + }}, + request_json(delete, uri(["bridges", BridgeID]), Config) ), - ok. + + %% Create non working bridge + BrokenURL = ?URL(Port + 1, "/foo"), + {ok, 201, BrokenBridge} = request( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(BrokenURL, Name), + fun json/1, + Config + ), + ?assertMatch( + #{ + <<"type">> := ?BRIDGE_TYPE_HTTP, + <<"name">> := Name, + <<"enable">> := true, + <<"status">> := <<"disconnected">>, + <<"status_reason">> := <<"Connection refused">>, + <<"node_status">> := [ + #{ + <<"status">> := <<"disconnected">>, + <<"status_reason">> := <<"Connection refused">> + } + | _ + ], + <<"url">> := BrokenURL + }, + BrokenBridge + ), + + {ok, 200, FixedBridge} = request_json( + put, + uri(["bridges", BridgeID]), + ?HTTP_BRIDGE(URL1), + Config + ), + ?assertMatch( + #{ + <<"status">> := <<"connected">>, + <<"node_status">> := [FixedNodeStatus = #{<<"status">> := <<"connected">>} | _] + } when + not is_map_key(<<"status_reason">>, FixedBridge) andalso + not is_map_key(<<"status_reason">>, FixedNodeStatus), + FixedBridge + ), + + %% Try create bridge with bad characters as name + {ok, 400, _} = request(post, uri(["bridges"]), ?HTTP_BRIDGE(URL1, <<"隋达"/utf8>>), Config), + + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), Config). t_http_bridges_local_topic(Config) -> Port = ?config(port, Config), %% assert we there's no bridges at first - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), %% then we add a webhook bridge, using POST %% POST /bridges/ will create a bridge @@ -354,21 +514,23 @@ t_http_bridges_local_topic(Config) -> {ok, 201, _} = request( post, uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name1) + ?HTTP_BRIDGE(URL1, Name1), + Config ), %% and we create another one without local_topic {ok, 201, _} = request( post, uri(["bridges"]), - maps:remove(<<"local_topic">>, ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name2)) + maps:remove(<<"local_topic">>, ?HTTP_BRIDGE(URL1, Name2)), + Config ), - BridgeID1 = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name1), - BridgeID2 = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name2), + BridgeID1 = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name1), + BridgeID2 = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name2), %% Send an message to emqx and the message should be forwarded to the HTTP server. %% This is to verify we can have 2 bridges with and without local_topic fields %% at the same time. Body = <<"my msg">>, - emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)), + _ = publish_message(<<"emqx_webhook/1">>, Body, Config), ?assert( receive {http_server, received, #{ @@ -385,26 +547,26 @@ t_http_bridges_local_topic(Config) -> end ), %% delete the bridge - {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID1]), []), - {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID2]), []), - ok. + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID1]), Config), + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID2]), Config). t_check_dependent_actions_on_delete(Config) -> Port = ?config(port, Config), %% assert we there's no bridges at first - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), %% then we add a webhook bridge, using POST %% POST /bridges/ will create a bridge URL1 = ?URL(Port, "path1"), Name = <<"t_http_crud_apis">>, - BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name), {ok, 201, _} = request( post, uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ?HTTP_BRIDGE(URL1, Name), + Config ), - {ok, 201, Rule} = request( + {ok, 201, #{<<"id">> := RuleId}} = request_json( post, uri(["rules"]), #{ @@ -412,37 +574,36 @@ t_check_dependent_actions_on_delete(Config) -> <<"enable">> => true, <<"actions">> => [BridgeID], <<"sql">> => <<"SELECT * from \"t\"">> - } + }, + Config ), - #{<<"id">> := RuleId} = emqx_json:decode(Rule, [return_maps]), %% deleting the bridge should fail because there is a rule that depends on it {ok, 400, _} = request( - delete, uri(["bridges", BridgeID]) ++ "?also_delete_dep_actions=false", [] + delete, uri(["bridges", BridgeID]) ++ "?also_delete_dep_actions=false", Config ), %% delete the rule first - {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []), + {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), Config), %% then delete the bridge is OK - {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), - - ok. + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), Config), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config). t_cascade_delete_actions(Config) -> Port = ?config(port, Config), %% assert we there's no bridges at first - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), %% then we add a webhook bridge, using POST %% POST /bridges/ will create a bridge URL1 = ?URL(Port, "path1"), Name = <<"t_http_crud_apis">>, - BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name), {ok, 201, _} = request( post, uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ?HTTP_BRIDGE(URL1, Name), + Config ), - {ok, 201, Rule} = request( + {ok, 201, #{<<"id">> := RuleId}} = request_json( post, uri(["rules"]), #{ @@ -450,27 +611,27 @@ t_cascade_delete_actions(Config) -> <<"enable">> => true, <<"actions">> => [BridgeID], <<"sql">> => <<"SELECT * from \"t\"">> - } + }, + Config ), - #{<<"id">> := RuleId} = emqx_json:decode(Rule, [return_maps]), %% delete the bridge will also delete the actions from the rules {ok, 204, _} = request( - delete, uri(["bridges", BridgeID]) ++ "?also_delete_dep_actions=true", [] + delete, + uri(["bridges", BridgeID]) ++ "?also_delete_dep_actions=true", + Config ), - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), - {ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), ?assertMatch( - #{ - <<"actions">> := [] - }, - emqx_json:decode(Rule1, [return_maps]) + {ok, 200, #{<<"actions">> := []}}, + request_json(get, uri(["rules", RuleId]), Config) ), - {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []), + {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), Config), {ok, 201, _} = request( post, uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ?HTTP_BRIDGE(URL1, Name), + Config ), {ok, 201, _} = request( post, @@ -480,12 +641,16 @@ t_cascade_delete_actions(Config) -> <<"enable">> => true, <<"actions">> => [BridgeID], <<"sql">> => <<"SELECT * from \"t\"">> - } + }, + Config ), - {ok, 204, _} = request(delete, uri(["bridges", BridgeID]) ++ "?also_delete_dep_actions", []), - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), - ok. + {ok, 204, _} = request( + delete, + uri(["bridges", BridgeID]) ++ "?also_delete_dep_actions", + Config + ), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config). t_broken_bpapi_vsn(Config) -> Port = ?config(port, Config), @@ -494,12 +659,13 @@ t_broken_bpapi_vsn(Config) -> {ok, 201, _Bridge} = request( post, uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ?HTTP_BRIDGE(URL1, Name), + Config ), - BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name), %% still works since we redirect to 'restart' - {ok, 501, <<>>} = request(post, operation_path(cluster, start, BridgeID), <<"">>), - {ok, 501, <<>>} = request(post, operation_path(node, start, BridgeID), <<"">>), + {ok, 501, <<>>} = request(post, {operation, cluster, start, BridgeID}, Config), + {ok, 501, <<>>} = request(post, {operation, node, start, BridgeID}, Config), ok. t_old_bpapi_vsn(Config) -> @@ -509,31 +675,34 @@ t_old_bpapi_vsn(Config) -> {ok, 201, _Bridge} = request( post, uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ?HTTP_BRIDGE(URL1, Name), + Config ), - BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), - {ok, 204, <<>>} = request(post, operation_path(cluster, stop, BridgeID), <<"">>), - {ok, 204, <<>>} = request(post, operation_path(node, stop, BridgeID), <<"">>), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name), + {ok, 204, <<>>} = request(post, {operation, cluster, stop, BridgeID}, Config), + {ok, 204, <<>>} = request(post, {operation, node, stop, BridgeID}, Config), %% still works since we redirect to 'restart' - {ok, 204, <<>>} = request(post, operation_path(cluster, start, BridgeID), <<"">>), - {ok, 204, <<>>} = request(post, operation_path(node, start, BridgeID), <<"">>), - {ok, 204, <<>>} = request(post, operation_path(cluster, restart, BridgeID), <<"">>), - {ok, 204, <<>>} = request(post, operation_path(node, restart, BridgeID), <<"">>), + {ok, 204, <<>>} = request(post, {operation, cluster, start, BridgeID}, Config), + {ok, 204, <<>>} = request(post, {operation, node, start, BridgeID}, Config), + {ok, 204, <<>>} = request(post, {operation, cluster, restart, BridgeID}, Config), + {ok, 204, <<>>} = request(post, {operation, node, restart, BridgeID}, Config), ok. -t_start_stop_bridges_node(Config) -> +t_start_bridge_unknown_node(Config) -> {ok, 404, _} = request( post, uri(["nodes", "thisbetterbenotanatomyet", "bridges", "webhook:foo", start]), - <<"">> + Config ), {ok, 404, _} = request( post, uri(["nodes", "undefined", "bridges", "webhook:foo", start]), - <<"">> - ), + Config + ). + +t_start_stop_bridges_node(Config) -> do_start_stop_bridges(node, Config). t_start_stop_bridges_cluster(Config) -> @@ -541,172 +710,250 @@ t_start_stop_bridges_cluster(Config) -> do_start_stop_bridges(Type, Config) -> %% assert we there's no bridges at first - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), Port = ?config(port, Config), URL1 = ?URL(Port, "abc"), Name = atom_to_binary(Type), - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ?assertMatch( + {ok, 201, #{ + <<"type">> := ?BRIDGE_TYPE_HTTP, + <<"name">> := Name, + <<"enable">> := true, + <<"status">> := <<"connected">>, + <<"node_status">> := [_ | _], + <<"url">> := URL1 + }}, + request_json( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, Name), + Config + ) ), - %ct:pal("the bridge ==== ~p", [Bridge]), - #{ - <<"type">> := ?BRIDGE_TYPE, - <<"name">> := Name, - <<"enable">> := true, - <<"status">> := <<"connected">>, - <<"node_status">> := [_ | _], - <<"url">> := URL1 - } = emqx_json:decode(Bridge, [return_maps]), - BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), - %% stop it - {ok, 204, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>), - {ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{<<"status">> := <<"stopped">>}, emqx_json:decode(Bridge2, [return_maps])), - %% start again - {ok, 204, <<>>} = request(post, operation_path(Type, start, BridgeID), <<"">>), - {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{<<"status">> := <<"connected">>}, emqx_json:decode(Bridge3, [return_maps])), - %% start a started bridge - {ok, 204, <<>>} = request(post, operation_path(Type, start, BridgeID), <<"">>), - {ok, 200, Bridge3_1} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{<<"status">> := <<"connected">>}, emqx_json:decode(Bridge3_1, [return_maps])), - %% restart an already started bridge - {ok, 204, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), - {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{<<"status">> := <<"connected">>}, emqx_json:decode(Bridge3, [return_maps])), - %% stop it again - {ok, 204, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>), - %% restart a stopped bridge - {ok, 204, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), - {ok, 200, Bridge4} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{<<"status">> := <<"connected">>}, emqx_json:decode(Bridge4, [return_maps])), - {ok, 404, _} = request(post, operation_path(Type, invalidop, BridgeID), <<"">>), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name), + ExpectedStatus = + case ?config(group, Config) of + cluster when Type == node -> + <<"inconsistent">>; + _ -> + <<"stopped">> + end, + + %% stop it + {ok, 204, <<>>} = request(post, {operation, Type, stop, BridgeID}, Config), + ?assertMatch( + {ok, 200, #{<<"status">> := ExpectedStatus}}, + request_json(get, uri(["bridges", BridgeID]), Config) + ), + %% start again + {ok, 204, <<>>} = request(post, {operation, Type, start, BridgeID}, Config), + ?assertMatch( + {ok, 200, #{<<"status">> := <<"connected">>}}, + request_json(get, uri(["bridges", BridgeID]), Config) + ), + %% start a started bridge + {ok, 204, <<>>} = request(post, {operation, Type, start, BridgeID}, Config), + ?assertMatch( + {ok, 200, #{<<"status">> := <<"connected">>}}, + request_json(get, uri(["bridges", BridgeID]), Config) + ), + %% restart an already started bridge + {ok, 204, <<>>} = request(post, {operation, Type, restart, BridgeID}, Config), + ?assertMatch( + {ok, 200, #{<<"status">> := <<"connected">>}}, + request_json(get, uri(["bridges", BridgeID]), Config) + ), + %% stop it again + {ok, 204, <<>>} = request(post, {operation, Type, stop, BridgeID}, Config), + %% restart a stopped bridge + {ok, 204, <<>>} = request(post, {operation, Type, restart, BridgeID}, Config), + ?assertMatch( + {ok, 200, #{<<"status">> := <<"connected">>}}, + request_json(get, uri(["bridges", BridgeID]), Config) + ), + + {ok, 404, _} = request(post, {operation, Type, invalidop, BridgeID}, Config), %% delete the bridge - {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), Config), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), %% Fail parse-id check - {ok, 404, _} = request(post, operation_path(Type, start, <<"wreckbook_fugazi">>), <<"">>), + {ok, 404, _} = request(post, {operation, Type, start, <<"wreckbook_fugazi">>}, Config), %% Looks ok but doesn't exist - {ok, 404, _} = request(post, operation_path(Type, start, <<"webhook:cptn_hook">>), <<"">>), + {ok, 404, _} = request(post, {operation, Type, start, <<"webhook:cptn_hook">>}, Config), %% Create broken bridge {ListenPort, Sock} = listen_on_random_port(), %% Connecting to this endpoint should always timeout BadServer = iolist_to_binary(io_lib:format("localhost:~B", [ListenPort])), BadName = <<"bad_", (atom_to_binary(Type))/binary>>, - {ok, 201, BadBridge1} = request( - post, - uri(["bridges"]), - ?MQTT_BRIDGE(BadServer, BadName) + ?assertMatch( + {ok, 201, #{ + <<"type">> := ?BRIDGE_TYPE_MQTT, + <<"name">> := BadName, + <<"enable">> := true, + <<"server">> := BadServer, + <<"status">> := <<"connecting">>, + <<"node_status">> := [_ | _] + }}, + request_json( + post, + uri(["bridges"]), + ?MQTT_BRIDGE(BadServer, BadName), + Config + ) ), - #{ - <<"type">> := ?BRIDGE_TYPE_MQTT, - <<"name">> := BadName, - <<"enable">> := true, - <<"server">> := BadServer, - <<"status">> := <<"connecting">>, - <<"node_status">> := [_ | _] - } = emqx_json:decode(BadBridge1, [return_maps]), BadBridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_MQTT, BadName), ?assertMatch( {ok, SC, _} when SC == 500 orelse SC == 503, - request(post, operation_path(Type, start, BadBridgeID), <<"">>) + request(post, {operation, Type, start, BadBridgeID}, Config) ), ok = gen_tcp:close(Sock), ok. +t_start_stop_inconsistent_bridge_node(Config) -> + start_stop_inconsistent_bridge(node, Config). + +t_start_stop_inconsistent_bridge_cluster(Config) -> + start_stop_inconsistent_bridge(cluster, Config). + +start_stop_inconsistent_bridge(Type, Config) -> + Port = ?config(port, Config), + URL = ?URL(Port, "abc"), + Node = ?config(api_node, Config), + + erpc:call(Node, fun() -> + meck:new(emqx_bridge_resource, [passthrough, no_link]), + meck:expect( + emqx_bridge_resource, + stop, + fun + (_, <<"bridge_not_found">>) -> {error, not_found}; + (BridgeType, Name) -> meck:passthrough([BridgeType, Name]) + end + ) + end), + + emqx_common_test_helpers:on_exit(fun() -> + erpc:call(Node, fun() -> + meck:unload([emqx_bridge_resource]) + end) + end), + + {ok, 201, _Bridge} = request( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL, <<"bridge_not_found">>), + Config + ), + {ok, 503, _} = request( + post, {operation, Type, stop, <<"webhook:bridge_not_found">>}, Config + ). + t_enable_disable_bridges(Config) -> %% assert we there's no bridges at first - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), Name = ?BRIDGE_NAME, Port = ?config(port, Config), URL1 = ?URL(Port, "abc"), - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ?assertMatch( + {ok, 201, #{ + <<"type">> := ?BRIDGE_TYPE_HTTP, + <<"name">> := Name, + <<"enable">> := true, + <<"status">> := <<"connected">>, + <<"node_status">> := [_ | _], + <<"url">> := URL1 + }}, + request_json( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, Name), + Config + ) ), - %ct:pal("the bridge ==== ~p", [Bridge]), - #{ - <<"type">> := ?BRIDGE_TYPE, - <<"name">> := Name, - <<"enable">> := true, - <<"status">> := <<"connected">>, - <<"node_status">> := [_ | _], - <<"url">> := URL1 - } = emqx_json:decode(Bridge, [return_maps]), - BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name), %% disable it - {ok, 204, <<>>} = request(put, enable_path(false, BridgeID), <<"">>), - {ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{<<"status">> := <<"stopped">>}, emqx_json:decode(Bridge2, [return_maps])), + {ok, 204, <<>>} = request(put, enable_path(false, BridgeID), Config), + ?assertMatch( + {ok, 200, #{<<"status">> := <<"stopped">>}}, + request_json(get, uri(["bridges", BridgeID]), Config) + ), %% enable again - {ok, 204, <<>>} = request(put, enable_path(true, BridgeID), <<"">>), - {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{<<"status">> := <<"connected">>}, emqx_json:decode(Bridge3, [return_maps])), + {ok, 204, <<>>} = request(put, enable_path(true, BridgeID), Config), + ?assertMatch( + {ok, 200, #{<<"status">> := <<"connected">>}}, + request_json(get, uri(["bridges", BridgeID]), Config) + ), %% enable an already started bridge - {ok, 204, <<>>} = request(put, enable_path(true, BridgeID), <<"">>), - {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{<<"status">> := <<"connected">>}, emqx_json:decode(Bridge3, [return_maps])), + {ok, 204, <<>>} = request(put, enable_path(true, BridgeID), Config), + ?assertMatch( + {ok, 200, #{<<"status">> := <<"connected">>}}, + request_json(get, uri(["bridges", BridgeID]), Config) + ), %% disable it again - {ok, 204, <<>>} = request(put, enable_path(false, BridgeID), <<"">>), + {ok, 204, <<>>} = request(put, enable_path(false, BridgeID), Config), %% bad param - {ok, 404, _} = request(put, enable_path(foo, BridgeID), <<"">>), - {ok, 404, _} = request(put, enable_path(true, "foo"), <<"">>), - {ok, 404, _} = request(put, enable_path(true, "webhook:foo"), <<"">>), + {ok, 404, _} = request(put, enable_path(foo, BridgeID), Config), + {ok, 404, _} = request(put, enable_path(true, "foo"), Config), + {ok, 404, _} = request(put, enable_path(true, "webhook:foo"), Config), - {ok, 400, Res} = request(post, operation_path(node, start, BridgeID), <<"">>), + {ok, 400, Res} = request(post, {operation, node, start, BridgeID}, <<>>, fun json/1, Config), ?assertEqual( - <<"{\"code\":\"BAD_REQUEST\",\"message\":\"Forbidden operation, bridge not enabled\"}">>, + #{ + <<"code">> => <<"BAD_REQUEST">>, + <<"message">> => <<"Forbidden operation, bridge not enabled">> + }, Res ), - {ok, 400, Res} = request(post, operation_path(cluster, start, BridgeID), <<"">>), + {ok, 400, Res} = request(post, {operation, cluster, start, BridgeID}, <<>>, fun json/1, Config), %% enable a stopped bridge - {ok, 204, <<>>} = request(put, enable_path(true, BridgeID), <<"">>), - {ok, 200, Bridge4} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{<<"status">> := <<"connected">>}, emqx_json:decode(Bridge4, [return_maps])), + {ok, 204, <<>>} = request(put, enable_path(true, BridgeID), Config), + ?assertMatch( + {ok, 200, #{<<"status">> := <<"connected">>}}, + request_json(get, uri(["bridges", BridgeID]), Config) + ), %% delete the bridge - {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []). + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), Config), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config). t_reset_bridges(Config) -> %% assert there's no bridges at first - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), Name = ?BRIDGE_NAME, Port = ?config(port, Config), URL1 = ?URL(Port, "abc"), - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ?assertMatch( + {ok, 201, #{ + <<"type">> := ?BRIDGE_TYPE_HTTP, + <<"name">> := Name, + <<"enable">> := true, + <<"status">> := <<"connected">>, + <<"node_status">> := [_ | _], + <<"url">> := URL1 + }}, + request_json( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, Name), + Config + ) ), - %ct:pal("the bridge ==== ~p", [Bridge]), - #{ - <<"type">> := ?BRIDGE_TYPE, - <<"name">> := Name, - <<"enable">> := true, - <<"status">> := <<"connected">>, - <<"node_status">> := [_ | _], - <<"url">> := URL1 - } = emqx_json:decode(Bridge, [return_maps]), - BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), - {ok, 204, <<>>} = request(put, uri(["bridges", BridgeID, "metrics/reset"]), []), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name), + {ok, 204, <<>>} = request(put, uri(["bridges", BridgeID, "metrics/reset"]), Config), %% delete the bridge - {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []). + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), Config), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config). -t_with_redact_update(_Config) -> +t_with_redact_update(Config) -> Name = <<"redact_update">>, Type = <<"mqtt">>, Password = <<"123456">>, @@ -723,20 +970,18 @@ t_with_redact_update(_Config) -> {ok, 201, _} = request( post, uri(["bridges"]), - Template + Template, + Config ), %% update with redacted config - Conf = emqx_misc:redact(Template), + BridgeConf = emqx_misc:redact(Template), BridgeID = emqx_bridge_resource:bridge_id(Type, Name), - {ok, 200, _ResBin} = request( - put, - uri(["bridges", BridgeID]), - Conf + {ok, 200, _} = request(put, uri(["bridges", BridgeID]), BridgeConf, Config), + ?assertEqual( + Password, + get_raw_config([bridges, Type, Name, password], Config) ), - RawConf = emqx:get_raw_config([bridges, Type, Name]), - Value = maps:get(<<"password">>, RawConf), - ?assertEqual(Password, Value), ok. t_bridges_probe(Config) -> @@ -746,59 +991,62 @@ t_bridges_probe(Config) -> {ok, 204, <<>>} = request( post, uri(["bridges_probe"]), - ?HTTP_BRIDGE(URL, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ?HTTP_BRIDGE(URL), + Config ), %% second time with same name is ok since no real bridge created {ok, 204, <<>>} = request( post, uri(["bridges_probe"]), - ?HTTP_BRIDGE(URL, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ?HTTP_BRIDGE(URL), + Config ), - {ok, 400, NxDomain} = request( - post, - uri(["bridges_probe"]), - ?HTTP_BRIDGE(<<"http://203.0.113.3:1234/foo">>, ?BRIDGE_TYPE, ?BRIDGE_NAME) - ), ?assertMatch( - #{ + {ok, 400, #{ <<"code">> := <<"TEST_FAILED">>, <<"message">> := _ - }, - emqx_json:decode(NxDomain, [return_maps]) + }}, + request_json( + post, + uri(["bridges_probe"]), + ?HTTP_BRIDGE(<<"http://203.0.113.3:1234/foo">>), + Config + ) ), {ok, 204, _} = request( post, uri(["bridges_probe"]), - ?MQTT_BRIDGE(<<"127.0.0.1:1883">>) + ?MQTT_BRIDGE(<<"127.0.0.1:1883">>), + Config ), - {ok, 400, ConnRefused} = request( - post, - uri(["bridges_probe"]), - ?MQTT_BRIDGE(<<"127.0.0.1:2883">>) - ), ?assertMatch( - #{ + {ok, 400, #{ <<"code">> := <<"TEST_FAILED">>, <<"message">> := <<"Connection refused">> - }, - emqx_json:decode(ConnRefused, [return_maps]) + }}, + request_json( + post, + uri(["bridges_probe"]), + ?MQTT_BRIDGE(<<"127.0.0.1:2883">>), + Config + ) ), - {ok, 400, HostNotFound} = request( - post, - uri(["bridges_probe"]), - ?MQTT_BRIDGE(<<"nohost:2883">>) - ), ?assertMatch( - #{ + {ok, 400, #{ <<"code">> := <<"TEST_FAILED">>, - <<"message">> := <<"Host not found">> - }, - emqx_json:decode(HostNotFound, [return_maps]) + <<"message">> := <<"Could not resolve host">> + }}, + request_json( + post, + uri(["bridges_probe"]), + ?MQTT_BRIDGE(<<"nohost:2883">>), + Config + ) ), AuthnConfig = #{ @@ -807,118 +1055,127 @@ t_bridges_probe(Config) -> <<"user_id_type">> => <<"username">> }, Chain = 'mqtt:global', - emqx:update_config( + {ok, _} = update_config( [authentication], - {create_authenticator, Chain, AuthnConfig} + {create_authenticator, Chain, AuthnConfig}, + Config ), User = #{user_id => <<"u">>, password => <<"p">>}, AuthenticatorID = <<"password_based:built_in_database">>, - {ok, _} = emqx_authentication:add_user( + {ok, _} = add_user_auth( Chain, AuthenticatorID, - User + User, + Config ), - {ok, 400, Unauthorized} = request( - post, - uri(["bridges_probe"]), - ?MQTT_BRIDGE(<<"127.0.0.1:1883">>)#{<<"proto_ver">> => <<"v4">>} - ), + emqx_common_test_helpers:on_exit(fun() -> + delete_user_auth(Chain, AuthenticatorID, User, Config) + end), + ?assertMatch( - #{ + {ok, 400, #{ <<"code">> := <<"TEST_FAILED">>, <<"message">> := <<"Unauthorized client">> - }, - emqx_json:decode(Unauthorized, [return_maps]) + }}, + request_json( + post, + uri(["bridges_probe"]), + ?MQTT_BRIDGE(<<"127.0.0.1:1883">>)#{<<"proto_ver">> => <<"v4">>}, + Config + ) ), - {ok, 400, Malformed} = request( - post, - uri(["bridges_probe"]), - ?MQTT_BRIDGE(<<"127.0.0.1:1883">>)#{ - <<"proto_ver">> => <<"v4">>, <<"password">> => <<"mySecret">>, <<"username">> => <<"u">> - } - ), ?assertMatch( - #{ + {ok, 400, #{ <<"code">> := <<"TEST_FAILED">>, - <<"message">> := <<"Malformed username or password">> - }, - emqx_json:decode(Malformed, [return_maps]) + <<"message">> := <<"Bad username or password">> + }}, + request_json( + post, + uri(["bridges_probe"]), + ?MQTT_BRIDGE(<<"127.0.0.1:1883">>)#{ + <<"proto_ver">> => <<"v4">>, + <<"password">> => <<"mySecret">>, + <<"username">> => <<"u">> + }, + Config + ) ), - {ok, 400, NotAuthorized} = request( - post, - uri(["bridges_probe"]), - ?MQTT_BRIDGE(<<"127.0.0.1:1883">>) - ), ?assertMatch( - #{ + {ok, 400, #{ <<"code">> := <<"TEST_FAILED">>, <<"message">> := <<"Not authorized">> - }, - emqx_json:decode(NotAuthorized, [return_maps]) + }}, + request_json( + post, + uri(["bridges_probe"]), + ?MQTT_BRIDGE(<<"127.0.0.1:1883">>), + Config + ) ), - {ok, 400, BadReq} = request( - post, - uri(["bridges_probe"]), - ?BRIDGE(<<"bad_bridge">>, <<"unknown_type">>) + ?assertMatch( + {ok, 400, #{<<"code">> := <<"BAD_REQUEST">>}}, + request_json( + post, + uri(["bridges_probe"]), + ?BRIDGE(<<"bad_bridge">>, <<"unknown_type">>), + Config + ) ), - ?assertMatch(#{<<"code">> := <<"BAD_REQUEST">>}, emqx_json:decode(BadReq, [return_maps])), ok. t_metrics(Config) -> Port = ?config(port, Config), %% assert we there's no bridges at first - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + {ok, 200, []} = request_json(get, uri(["bridges"]), Config), %% then we add a webhook bridge, using POST %% POST /bridges/ will create a bridge URL1 = ?URL(Port, "path1"), Name = ?BRIDGE_NAME, - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ?assertMatch( + {ok, 201, + Bridge = #{ + <<"type">> := ?BRIDGE_TYPE_HTTP, + <<"name">> := Name, + <<"enable">> := true, + <<"status">> := _, + <<"node_status">> := [_ | _], + <<"url">> := URL1 + }} when + %% assert that the bridge return doesn't contain metrics anymore + not is_map_key(<<"metrics">>, Bridge) andalso + not is_map_key(<<"node_metrics">>, Bridge), + request_json( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, Name), + Config + ) ), - %ct:pal("---bridge: ~p", [Bridge]), - Decoded = emqx_json:decode(Bridge, [return_maps]), - #{ - <<"type">> := ?BRIDGE_TYPE, - <<"name">> := Name, - <<"enable">> := true, - <<"status">> := _, - <<"node_status">> := [_ | _], - <<"url">> := URL1 - } = Decoded, - - %% assert that the bridge return doesn't contain metrics anymore - ?assertNot(maps:is_key(<<"metrics">>, Decoded)), - ?assertNot(maps:is_key(<<"node_metrics">>, Decoded)), - - BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_HTTP, Name), %% check for empty bridge metrics - {ok, 200, Bridge1Str} = request(get, uri(["bridges", BridgeID, "metrics"]), []), ?assertMatch( - #{ + {ok, 200, #{ <<"metrics">> := #{<<"success">> := 0}, <<"node_metrics">> := [_ | _] - }, - emqx_json:decode(Bridge1Str, [return_maps]) + }}, + request_json(get, uri(["bridges", BridgeID, "metrics"]), Config) ), %% check that the bridge doesn't contain metrics anymore - {ok, 200, Bridge2Str} = request(get, uri(["bridges", BridgeID]), []), - Decoded2 = emqx_json:decode(Bridge2Str, [return_maps]), - ?assertNot(maps:is_key(<<"metrics">>, Decoded2)), - ?assertNot(maps:is_key(<<"node_metrics">>, Decoded2)), + {ok, 200, Bridge} = request_json(get, uri(["bridges", BridgeID]), Config), + ?assertNot(maps:is_key(<<"metrics">>, Bridge)), + ?assertNot(maps:is_key(<<"node_metrics">>, Bridge)), %% send an message to emqx and the message should be forwarded to the HTTP server Body = <<"my msg">>, - emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)), + _ = publish_message(<<"emqx_webhook/1">>, Body, Config), ?assert( receive {http_server, received, #{ @@ -936,21 +1193,20 @@ t_metrics(Config) -> ), %% check for non-empty bridge metrics - {ok, 200, Bridge3Str} = request(get, uri(["bridges", BridgeID, "metrics"]), []), ?assertMatch( - #{ + {ok, 200, #{ <<"metrics">> := #{<<"success">> := _}, <<"node_metrics">> := [_ | _] - }, - emqx_json:decode(Bridge3Str, [return_maps]) + }}, + request_json(get, uri(["bridges", BridgeID, "metrics"]), Config) ), %% check that metrics isn't returned when listing all bridges - {ok, 200, BridgesStr} = request(get, uri(["bridges"]), []), + {ok, 200, Bridges} = request_json(get, uri(["bridges"]), Config), ?assert( lists:all( fun(E) -> not maps:is_key(<<"metrics">>, E) end, - emqx_json:decode(BridgesStr, [return_maps]) + Bridges ) ), ok. @@ -963,34 +1219,86 @@ t_inconsistent_webhook_request_timeouts(Config) -> Name = ?BRIDGE_NAME, BadBridgeParams = emqx_map_lib:deep_merge( - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name), + ?HTTP_BRIDGE(URL1, Name), #{ <<"request_timeout">> => <<"1s">>, <<"resource_opts">> => #{<<"request_timeout">> => <<"2s">>} } ), - {ok, 201, RawResponse} = request( - post, - uri(["bridges"]), - BadBridgeParams - ), - %% note: same value on both fields ?assertMatch( - #{ + {ok, 201, #{ + %% note: same value on both fields <<"request_timeout">> := <<"2s">>, <<"resource_opts">> := #{<<"request_timeout">> := <<"2s">>} - }, - emqx_json:decode(RawResponse, [return_maps]) + }}, + request_json( + post, + uri(["bridges"]), + BadBridgeParams, + Config + ) ), ok. -operation_path(node, Oper, BridgeID) -> - uri(["nodes", node(), "bridges", BridgeID, Oper]); -operation_path(cluster, Oper, BridgeID) -> +%% + +request(Method, URL, Config) -> + request(Method, URL, [], Config). + +request(Method, {operation, Type, Op, BridgeID}, Body, Config) -> + URL = operation_path(Type, Op, BridgeID, Config), + request(Method, URL, Body, Config); +request(Method, URL, Body, Config) -> + Opts = #{compatible_mode => true, httpc_req_opts => [{body_format, binary}]}, + emqx_mgmt_api_test_util:request_api(Method, URL, [], auth_header(Config), Body, Opts). + +request(Method, URL, Body, Decoder, Config) -> + case request(Method, URL, Body, Config) of + {ok, Code, Response} -> + {ok, Code, Decoder(Response)}; + Otherwise -> + Otherwise + end. + +request_json(Method, URLLike, Config) -> + request(Method, URLLike, [], fun json/1, Config). + +request_json(Method, URLLike, Body, Config) -> + request(Method, URLLike, Body, fun json/1, Config). + +auth_header(Config) -> + erpc:call(?config(api_node, Config), emqx_common_test_http, default_auth_header, []). + +operation_path(node, Oper, BridgeID, Config) -> + uri(["nodes", ?config(api_node, Config), "bridges", BridgeID, Oper]); +operation_path(cluster, Oper, BridgeID, _Config) -> uri(["bridges", BridgeID, Oper]). enable_path(Enable, BridgeID) -> uri(["bridges", BridgeID, "enable", Enable]). +publish_message(Topic, Body, Config) -> + Node = ?config(api_node, Config), + erpc:call(Node, emqx, publish, [emqx_message:make(Topic, Body)]). + +update_config(Path, Value, Config) -> + Node = ?config(api_node, Config), + erpc:call(Node, emqx, update_config, [Path, Value]). + +get_raw_config(Path, Config) -> + Node = ?config(api_node, Config), + erpc:call(Node, emqx, get_raw_config, [Path]). + +add_user_auth(Chain, AuthenticatorID, User, Config) -> + Node = ?config(api_node, Config), + erpc:call(Node, emqx_authentication, add_user, [Chain, AuthenticatorID, User]). + +delete_user_auth(Chain, AuthenticatorID, User, Config) -> + Node = ?config(api_node, Config), + erpc:call(Node, emqx_authentication, delete_user, [Chain, AuthenticatorID, User]). + str(S) when is_list(S) -> S; str(S) when is_binary(S) -> binary_to_list(S). + +json(B) when is_binary(B) -> + emqx_json:decode(B, [return_maps]). diff --git a/apps/emqx_bridge/test/emqx_bridge_webhook_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_webhook_SUITE.erl index f249aa95e..e222190d2 100644 --- a/apps/emqx_bridge/test/emqx_bridge_webhook_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_webhook_SUITE.erl @@ -172,7 +172,7 @@ bridge_async_config(#{port := Port} = Config) -> " request_timeout = \"~ps\"\n" " body = \"${id}\"" " resource_opts {\n" - " async_inflight_window = 100\n" + " inflight_window = 100\n" " auto_restart_interval = \"60s\"\n" " health_check_interval = \"15s\"\n" " max_queue_bytes = \"1GB\"\n" diff --git a/apps/emqx_conf/src/emqx_cluster_rpc.erl b/apps/emqx_conf/src/emqx_cluster_rpc.erl index 89f678554..f7c34031c 100644 --- a/apps/emqx_conf/src/emqx_cluster_rpc.erl +++ b/apps/emqx_conf/src/emqx_cluster_rpc.erl @@ -275,8 +275,13 @@ init([Node, RetryMs]) -> _ = mria:wait_for_tables([?CLUSTER_MFA, ?CLUSTER_COMMIT]), {ok, _} = mnesia:subscribe({table, ?CLUSTER_MFA, simple}), State = #{node => Node, retry_interval => RetryMs}, + %% The init transaction ID is set in emqx_conf_app after + %% it has fetched the latest config from one of the core nodes TnxId = emqx_app:get_init_tnx_id(), ok = maybe_init_tnx_id(Node, TnxId), + %% Now continue with the normal catch-up process + %% That is: apply the missing transactions after the config + %% was copied until now. {ok, State, {continue, ?CATCH_UP}}. %% @private @@ -396,6 +401,7 @@ get_cluster_tnx_id() -> Id -> Id end. +%% The entry point of a config change transaction. init_mfa(Node, MFA) -> mnesia:write_lock_table(?CLUSTER_MFA), LatestId = get_cluster_tnx_id(), diff --git a/apps/emqx_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl index 33214946d..d03cf9c27 100644 --- a/apps/emqx_conf/src/emqx_conf.erl +++ b/apps/emqx_conf/src/emqx_conf.erl @@ -156,7 +156,15 @@ dump_schema(Dir, SchemaModule, I18nFile) -> gen_schema_json(Dir, I18nFile, SchemaModule, Lang) -> SchemaJsonFile = filename:join([Dir, "schema-" ++ Lang ++ ".json"]), io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]), - Opts = #{desc_file => I18nFile, lang => Lang}, + %% EMQX_SCHEMA_FULL_DUMP is quite a hidden API + %% it is used to dump the full schema for EMQX developers and supporters + IncludeImportance = + case os:getenv("EMQX_SCHEMA_FULL_DUMP") =:= "1" of + true -> ?IMPORTANCE_HIDDEN; + false -> ?IMPORTANCE_LOW + end, + io:format(user, "===< Including fields from importance level: ~p~n", [IncludeImportance]), + Opts = #{desc_file => I18nFile, lang => Lang, include_importance_up_from => IncludeImportance}, JsonMap = hocon_schema_json:gen(SchemaModule, Opts), IoData = jsx:encode(JsonMap, [space, {indent, 4}]), ok = file:write_file(SchemaJsonFile, IoData). @@ -220,7 +228,8 @@ gen_example(File, SchemaModule, I18nFile, Lang) -> title => <<"EMQX Configuration Example">>, body => <<"">>, desc_file => I18nFile, - lang => Lang + lang => Lang, + include_importance_up_from => ?IMPORTANCE_MEDIUM }, Example = hocon_schema_example:gen(SchemaModule, Opts), file:write_file(File, Example). diff --git a/apps/emqx_conf/src/emqx_conf_app.erl b/apps/emqx_conf/src/emqx_conf_app.erl index af42f0e1a..c2e1e0e62 100644 --- a/apps/emqx_conf/src/emqx_conf_app.erl +++ b/apps/emqx_conf/src/emqx_conf_app.erl @@ -28,7 +28,18 @@ -define(DEFAULT_INIT_TXN_ID, -1). start(_StartType, _StartArgs) -> - init_conf(), + try + ok = init_conf() + catch + C:E:St -> + ?SLOG(critical, #{ + msg => failed_to_init_config, + exception => C, + reason => E, + stacktrace => St + }), + init:stop(1) + end, ok = emqx_config_logger:refresh_config(), emqx_conf_sup:start_link(). @@ -85,9 +96,9 @@ init_load() -> init_conf() -> {ok, TnxId} = copy_override_conf_from_core_node(), - emqx_app:set_init_tnx_id(TnxId), - init_load(), - emqx_app:set_init_config_load_done(). + _ = emqx_app:set_init_tnx_id(TnxId), + ok = init_load(), + ok = emqx_app:set_init_config_load_done(). cluster_nodes() -> mria_mnesia:running_nodes() -- [node()]. diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 4862be5fe..58bcf9700 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -397,6 +397,7 @@ fields("node") -> #{ default => <<"emqx@127.0.0.1">>, 'readOnly' => true, + importance => ?IMPORTANCE_HIGH, desc => ?DESC(node_name) } )}, @@ -409,6 +410,7 @@ fields("node") -> 'readOnly' => true, sensitive => true, desc => ?DESC(node_cookie), + importance => ?IMPORTANCE_HIGH, converter => fun emqx_schema:password_converter/2 } )}, @@ -419,6 +421,7 @@ fields("node") -> mapping => "vm_args.+P", desc => ?DESC(process_limit), default => 2097152, + importance => ?IMPORTANCE_MEDIUM, 'readOnly' => true } )}, @@ -429,6 +432,7 @@ fields("node") -> mapping => "vm_args.+Q", desc => ?DESC(max_ports), default => 1048576, + importance => ?IMPORTANCE_HIGH, 'readOnly' => true } )}, @@ -439,6 +443,7 @@ fields("node") -> mapping => "vm_args.+zdbbl", desc => ?DESC(dist_buffer_size), default => 8192, + importance => ?IMPORTANCE_LOW, 'readOnly' => true } )}, @@ -449,6 +454,7 @@ fields("node") -> mapping => "vm_args.+e", desc => ?DESC(max_ets_tables), default => 262144, + importance => ?IMPORTANCE_LOW, 'readOnly' => true } )}, @@ -459,6 +465,10 @@ fields("node") -> required => true, 'readOnly' => true, mapping => "emqx.data_dir", + %% for now, it's tricky to use a different data_dir + %% otherwise data paths in cluster config may differ + %% TODO: change configurable data file paths to relative + importance => ?IMPORTANCE_HIDDEN, desc => ?DESC(node_data_dir) } )}, @@ -467,7 +477,7 @@ fields("node") -> hoconsc:array(string()), #{ mapping => "emqx.config_files", - hidden => true, + importance => ?IMPORTANCE_HIDDEN, required => false, 'readOnly' => true } @@ -479,6 +489,7 @@ fields("node") -> mapping => "emqx_machine.global_gc_interval", default => <<"15m">>, desc => ?DESC(node_global_gc_interval), + importance => ?IMPORTANCE_LOW, 'readOnly' => true } )}, @@ -489,6 +500,7 @@ fields("node") -> mapping => "vm_args.-env ERL_CRASH_DUMP", desc => ?DESC(node_crash_dump_file), default => crash_dump_file_default(), + importance => ?IMPORTANCE_LOW, 'readOnly' => true } )}, @@ -499,6 +511,7 @@ fields("node") -> mapping => "vm_args.-env ERL_CRASH_DUMP_SECONDS", default => <<"30s">>, desc => ?DESC(node_crash_dump_seconds), + importance => ?IMPORTANCE_LOW, 'readOnly' => true } )}, @@ -509,6 +522,7 @@ fields("node") -> mapping => "vm_args.-env ERL_CRASH_DUMP_BYTES", default => <<"100MB">>, desc => ?DESC(node_crash_dump_bytes), + importance => ?IMPORTANCE_LOW, 'readOnly' => true } )}, @@ -519,6 +533,7 @@ fields("node") -> mapping => "vm_args.-kernel net_ticktime", default => <<"2m">>, 'readOnly' => true, + importance => ?IMPORTANCE_LOW, desc => ?DESC(node_dist_net_ticktime) } )}, @@ -529,6 +544,7 @@ fields("node") -> mapping => "emqx_machine.backtrace_depth", default => 23, 'readOnly' => true, + importance => ?IMPORTANCE_LOW, desc => ?DESC(node_backtrace_depth) } )}, @@ -539,6 +555,7 @@ fields("node") -> mapping => "emqx_machine.applications", default => [], 'readOnly' => true, + importance => ?IMPORTANCE_LOW, desc => ?DESC(node_applications) } )}, @@ -548,13 +565,17 @@ fields("node") -> #{ desc => ?DESC(node_etc_dir), 'readOnly' => true, + importance => ?IMPORTANCE_LOW, deprecated => {since, "5.0.8"} } )}, {"cluster_call", sc( ?R_REF("cluster_call"), - #{'readOnly' => true} + #{ + 'readOnly' => true, + importance => ?IMPORTANCE_LOW + } )}, {"db_backend", sc( @@ -563,6 +584,7 @@ fields("node") -> mapping => "mria.db_backend", default => rlog, 'readOnly' => true, + importance => ?IMPORTANCE_HIDDEN, desc => ?DESC(db_backend) } )}, @@ -573,6 +595,7 @@ fields("node") -> mapping => "mria.node_role", default => core, 'readOnly' => true, + importance => ?IMPORTANCE_HIGH, desc => ?DESC(db_role) } )}, @@ -583,6 +606,7 @@ fields("node") -> mapping => "mria.rlog_rpc_module", default => gen_rpc, 'readOnly' => true, + importance => ?IMPORTANCE_HIDDEN, desc => ?DESC(db_rpc_module) } )}, @@ -593,6 +617,7 @@ fields("node") -> mapping => "mria.tlog_push_mode", default => async, 'readOnly' => true, + importance => ?IMPORTANCE_LOW, desc => ?DESC(db_tlog_push_mode) } )}, @@ -601,7 +626,7 @@ fields("node") -> hoconsc:enum([gen_rpc, distr]), #{ mapping => "mria.shard_transport", - hidden => true, + importance => ?IMPORTANCE_HIDDEN, default => gen_rpc, desc => ?DESC(db_default_shard_transport) } @@ -611,7 +636,7 @@ fields("node") -> map(shard, hoconsc:enum([gen_rpc, distr])), #{ desc => ?DESC(db_shard_transports), - hidden => true, + importance => ?IMPORTANCE_HIDDEN, mapping => "emqx_machine.custom_shard_transports", default => #{} } diff --git a/apps/emqx_ctl/README.md b/apps/emqx_ctl/README.md index a91342606..2638031e6 100644 --- a/apps/emqx_ctl/README.md +++ b/apps/emqx_ctl/README.md @@ -1,4 +1,41 @@ -emqx_ctl -===== +# emqx_ctl -Backend module for `emqx_ctl` command. +This application accepts dynamic `emqx ctl` command registrations so plugins can add their own commands. +Please note that the 'proxy' command `emqx_ctl` is considered deprecated, going forward, please use `emqx ctl` instead. + +## Add a new command + +To add a new command, the application must implement a callback function to handle the command, and register the command with `emqx_ctl:register_command/2` API. + +### Register + +To add a new command which can be executed from `emqx ctl`, the application must call `emqx_ctl:register_command/2` API to register the command. + +For example, to add a new command `myplugin` which is to be executed as `emqx ctl myplugin`, the application must call `emqx_ctl:register_command/2` API as follows: + +```erlang +emqx_ctl:register_command(mypluin, {myplugin_cli, cmd}). +``` + +### Callback + +The callback function must be exported by the application and must have the following signature: + +```erlang +cmd([Arg1, Arg2, ...]) -> ok. +``` + +It must also implement a special clause to handle the `usage` argument: + +```erlang +cmd([usage]) -> "myplugin [arg1] [arg2] ..."; +``` + +### Utility + +The `emqx_ctl` application provides some utility functions which help to format the output of the command. +For example `emqx_ctl:print/2` and `emqx_ctl:usage/1`. + +## Reference + +[emqx_management_cli](../emqx_management/src/emqx_mgmt_cli.erl) can be taken as a reference for how to implement a command. diff --git a/apps/emqx_dashboard/README.md b/apps/emqx_dashboard/README.md index 7466b5afe..88c714aca 100644 --- a/apps/emqx_dashboard/README.md +++ b/apps/emqx_dashboard/README.md @@ -1 +1,17 @@ -# TODO: Doc +# EMQX Dashboard + +This application provides access to the EMQX Dashboard as well as the actual, +underlying REST API itself and provides authorization to protect against +unauthorized access. Furthermore it connects middleware adding CORS headers. +Last but not least it exposes the `/status` endpoint needed for healtcheck +monitoring. + +## Implementation details + +This implementation is based on `minirest`, and relies on `hoconsc` to provide an +OpenAPI spec for `swagger`. + +Note, at this point EMQX Dashboard itself is an independent frontend project and +is integrated through a static file handler. This code here is responsible to +provide an HTTP(S) server to give access to it and its underlying API calls. +This includes user management and login for the frontend. diff --git a/apps/emqx_dashboard/src/emqx_dashboard.app.src b/apps/emqx_dashboard/src/emqx_dashboard.app.src index 3970d76e4..8a4764c84 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.app.src +++ b/apps/emqx_dashboard/src/emqx_dashboard.app.src @@ -2,7 +2,7 @@ {application, emqx_dashboard, [ {description, "EMQX Web Dashboard"}, % strict semver, bump manually! - {vsn, "5.0.15"}, + {vsn, "5.0.16"}, {modules, []}, {registered, [emqx_dashboard_sup]}, {applications, [kernel, stdlib, mnesia, minirest, emqx, emqx_ctl]}, diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index f0344dd5a..6f0c8334a 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -262,7 +262,7 @@ i18n_file() -> end. listeners() -> - emqx_conf:get([dashboard, listeners], []). + emqx_conf:get([dashboard, listeners], #{}). api_key_authorize(Req, Key, Secret) -> Path = cowboy_req:path(Req), diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index cc2a1337d..d5655d99d 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -74,7 +74,7 @@ schema("/login") -> post => #{ tags => [<<"dashboard">>], desc => ?DESC(login_api), - summary => <<"Dashboard Auth">>, + summary => <<"Dashboard authentication">>, 'requestBody' => fields([username, password]), responses => #{ 200 => fields([token, version, license]), diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl index 69f5bf34e..aaee92b8c 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl @@ -121,32 +121,27 @@ fields(sampler_current) -> monitor(get, #{query_string := QS, bindings := Bindings}) -> Latest = maps:get(<<"latest">>, QS, infinity), - RawNode = maps:get(node, Bindings, all), - with_node(RawNode, dashboard_samplers_fun(Latest)). + RawNode = maps:get(node, Bindings, <<"all">>), + emqx_api_lib:with_node_or_cluster(RawNode, dashboard_samplers_fun(Latest)). dashboard_samplers_fun(Latest) -> fun(NodeOrCluster) -> case emqx_dashboard_monitor:samplers(NodeOrCluster, Latest) of - {badrpc, _} = Error -> Error; + {badrpc, _} = Error -> {error, Error}; Samplers -> {ok, Samplers} end end. monitor_current(get, #{bindings := Bindings}) -> - RawNode = maps:get(node, Bindings, all), - with_node(RawNode, fun emqx_dashboard_monitor:current_rate/1). + RawNode = maps:get(node, Bindings, <<"all">>), + emqx_api_lib:with_node_or_cluster(RawNode, fun current_rate/1). -with_node(RawNode, Fun) -> - case emqx_misc:safe_to_existing_atom(RawNode, utf8) of - {ok, NodeOrCluster} -> - case Fun(NodeOrCluster) of - {badrpc, {Node, Reason}} -> - {404, 'NOT_FOUND', io_lib:format("Node not found: ~p (~p)", [Node, Reason])}; - {ok, Result} -> - {200, Result} - end; - _Error -> - {404, 'NOT_FOUND', io_lib:format("Node not found: ~p", [RawNode])} +current_rate(Node) -> + case emqx_dashboard_monitor:current_rate(Node) of + {badrpc, _} = BadRpc -> + {error, BadRpc}; + {ok, _} = OkResult -> + OkResult end. %% ------------------------------------------------------------------------------------------------- diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index 77fcd4f76..e2872c0d7 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -457,7 +457,18 @@ trans_description(Spec, Hocon) -> Spec; Desc -> Desc1 = binary:replace(Desc, [<<"\n">>], <<"
">>, [global]), - Spec#{description => Desc1} + maybe_add_summary_from_label(Spec#{description => Desc1}, Hocon) + end. + +maybe_add_summary_from_label(Spec, Hocon) -> + Label = + case desc_struct(Hocon) of + ?DESC(_, _) = Struct -> get_i18n(<<"label">>, Struct, undefined); + _ -> undefined + end, + case Label of + undefined -> Spec; + _ -> Spec#{summary => Label} end. get_i18n(Key, Struct, Default) -> @@ -819,36 +830,8 @@ to_bin(X) -> X. parse_object(PropList = [_ | _], Module, Options) when is_list(PropList) -> - {Props, Required, Refs} = - lists:foldl( - fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) -> - NameBin = to_bin(Name), - case hoconsc:is_schema(Hocon) of - true -> - HoconType = hocon_schema:field_schema(Hocon, type), - Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon), - SchemaToSpec = schema_converter(Options), - Init = trans_desc(Init0, Hocon, SchemaToSpec, NameBin), - {Prop, Refs1} = SchemaToSpec(HoconType, Module), - NewRequiredAcc = - case is_required(Hocon) of - true -> [NameBin | RequiredAcc]; - false -> RequiredAcc - end, - { - [{NameBin, maps:merge(Prop, Init)} | Acc], - NewRequiredAcc, - Refs1 ++ RefsAcc - }; - false -> - {SubObject, SubRefs} = parse_object(Hocon, Module, Options), - {[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc} - end - end, - {[], [], []}, - PropList - ), - Object = #{<<"type">> => object, <<"properties">> => lists:reverse(Props)}, + {Props, Required, Refs} = parse_object_loop(PropList, Module, Options), + Object = #{<<"type">> => object, <<"properties">> => Props}, case Required of [] -> {Object, Refs}; _ -> {maps:put(required, Required, Object), Refs} @@ -863,6 +846,54 @@ parse_object(Other, Module, Options) -> }} ). +parse_object_loop(PropList0, Module, Options) -> + PropList = lists:filter( + fun({_, Hocon}) -> + case hoconsc:is_schema(Hocon) andalso is_hidden(Hocon) of + true -> false; + false -> true + end + end, + PropList0 + ), + parse_object_loop(PropList, Module, Options, _Props = [], _Required = [], _Refs = []). + +parse_object_loop([], _Modlue, _Options, Props, Required, Refs) -> + {lists:reverse(Props), lists:usort(Required), Refs}; +parse_object_loop([{Name, Hocon} | Rest], Module, Options, Props, Required, Refs) -> + NameBin = to_bin(Name), + case hoconsc:is_schema(Hocon) of + true -> + HoconType = hocon_schema:field_schema(Hocon, type), + Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon), + SchemaToSpec = schema_converter(Options), + Init = trans_desc(Init0, Hocon, SchemaToSpec, NameBin), + {Prop, Refs1} = SchemaToSpec(HoconType, Module), + NewRequiredAcc = + case is_required(Hocon) of + true -> [NameBin | Required]; + false -> Required + end, + parse_object_loop( + Rest, + Module, + Options, + [{NameBin, maps:merge(Prop, Init)} | Props], + NewRequiredAcc, + Refs1 ++ Refs + ); + false -> + %% TODO: there is only a handful of such + %% refactor the schema to unify the two cases + {SubObject, SubRefs} = parse_object(Hocon, Module, Options), + parse_object_loop( + Rest, Module, Options, [{NameBin, SubObject} | Props], Required, SubRefs ++ Refs + ) + end. + +%% return true if the field has 'importance' set to 'hidden' +is_hidden(Hocon) -> + hocon_schema:is_hidden(Hocon, #{include_importance_up_from => ?IMPORTANCE_LOW}). is_required(Hocon) -> hocon_schema:field_schema(Hocon, required) =:= true. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index 906d57e9d..e951a9a2a 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -56,30 +56,12 @@ all() -> emqx_common_test_helpers:all(?MODULE). -end_suite() -> - end_suite([]). - -end_suite(Apps) -> - application:unload(emqx_management), - mnesia:clear_table(?ADMIN), - emqx_common_test_helpers:stop_apps(Apps ++ [emqx_dashboard]). - init_per_suite(Config) -> - emqx_common_test_helpers:start_apps( - [emqx_management, emqx_dashboard], - fun set_special_configs/1 - ), + emqx_mgmt_api_test_util:init_suite([emqx_management]), Config. end_per_suite(_Config) -> - emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_management]), - mria:stop(). - -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(), - ok; -set_special_configs(_) -> - ok. + emqx_mgmt_api_test_util:end_suite([emqx_management]). t_overview(_) -> mnesia:clear_table(?ADMIN), diff --git a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl index 9ae5d4418..c12849ac7 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl @@ -19,36 +19,22 @@ -compile(export_all). -include("emqx_dashboard.hrl"). --include_lib("emqx/include/http_api.hrl"). -include_lib("eunit/include/eunit.hrl"). all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> - mria:start(), - application:load(emqx_dashboard), - emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1), + emqx_mgmt_api_test_util:init_suite([emqx_conf]), Config. -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(), - ok; -set_special_configs(_) -> - ok. - -end_per_suite(Config) -> - end_suite(), - Config. +end_per_suite(_Config) -> + emqx_mgmt_api_test_util:end_suite([emqx_conf]). end_per_testcase(_, _Config) -> All = emqx_dashboard_admin:all_users(), [emqx_dashboard_admin:remove_user(Name) || #{username := Name} <- All]. -end_suite() -> - application:unload(emqx_management), - emqx_common_test_helpers:stop_apps([emqx_dashboard]). - t_check_user(_) -> Username = <<"admin1">>, Password = <<"public_1">>, diff --git a/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl index a9b448662..92327a7db 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl @@ -31,15 +31,10 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> - mria:start(), emqx_mgmt_api_test_util:init_suite([emqx_conf]), Config. -end_per_suite(Config) -> - end_suite(), - Config. - -end_suite() -> +end_per_suite(_Config) -> emqx_mgmt_api_test_util:end_suite([emqx_conf]). t_bad_api_path(_) -> diff --git a/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl index 5def3c9dd..19d3f471e 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl @@ -29,24 +29,11 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> - mria:start(), - application:load(emqx_dashboard), - emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1), + emqx_mgmt_api_test_util:init_suite([emqx_conf]), Config. -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(), - ok; -set_special_configs(_) -> - ok. - -end_per_suite(Config) -> - end_suite(), - Config. - -end_suite() -> - application:unload(emqx_management), - emqx_common_test_helpers:stop_apps([emqx_dashboard]). +end_per_suite(_Config) -> + emqx_mgmt_api_test_util:end_suite([emqx_conf]). t_all_code(_) -> HrlDef = ?ERROR_CODES, diff --git a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl index a05de339b..cb6a5a9fd 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl @@ -26,10 +26,7 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> - emqx_common_test_helpers:start_apps( - [emqx_management, emqx_dashboard], - fun set_special_configs/1 - ), + emqx_mgmt_api_test_util:init_suite([emqx_management], fun set_special_configs/1), Config. set_special_configs(emqx_dashboard) -> @@ -38,12 +35,8 @@ set_special_configs(emqx_dashboard) -> set_special_configs(_) -> ok. -end_per_suite(Config) -> - application:unload(emqx_management), - mnesia:clear_table(?ADMIN), - emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_management]), - mria:stop(), - Config. +end_per_suite(_Config) -> + emqx_mgmt_api_test_util:end_suite([emqx_management]). t_status(_Config) -> ProxyInfo = #{ diff --git a/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl index bfbd9b973..f35652f8e 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl @@ -31,20 +31,11 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> - application:load(emqx_dashboard), - mria:start(), - emqx_common_test_helpers:start_apps([emqx_dashboard], fun set_special_configs/1), + emqx_mgmt_api_test_util:init_suite([]), Config. -end_per_suite(Config) -> - emqx_common_test_helpers:stop_apps([emqx_dashboard]), - Config. - -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(), - ok; -set_special_configs(_) -> - ok. +end_per_suite(_Config) -> + emqx_mgmt_api_test_util:end_suite([]). t_monitor_samplers_all(_Config) -> timer:sleep(?DEFAULT_SAMPLE_INTERVAL * 2 * 1000 + 20), diff --git a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl index 5d89fb273..472e90405 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl @@ -63,25 +63,12 @@ groups() -> ]. init_per_suite(Config) -> - mria:start(), - application:load(emqx_dashboard), - emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1), + emqx_mgmt_api_test_util:init_suite([emqx_conf]), emqx_dashboard:init_i18n(), Config. -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(), - ok; -set_special_configs(_) -> - ok. - -end_per_suite(Config) -> - end_suite(), - Config. - -end_suite() -> - application:unload(emqx_management), - emqx_common_test_helpers:stop_apps([emqx_dashboard]). +end_per_suite(_Config) -> + emqx_mgmt_api_test_util:end_suite([emqx_conf]). t_in_path(_Config) -> Expect = diff --git a/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl index 717a7d4ca..3150ed097 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl @@ -61,7 +61,7 @@ t_object(_Config) -> #{ <<"schema">> => #{ - required => [<<"timeout">>, <<"per_page">>], + required => [<<"per_page">>, <<"timeout">>], <<"properties">> => [ {<<"per_page">>, #{ description => <<"good per page desc">>, diff --git a/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl index c9cfba254..4d1501dae 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl @@ -32,25 +32,17 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> - mria:start(), - application:load(emqx_dashboard), - emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1), + emqx_mgmt_api_test_util:init_suite([emqx_conf]), emqx_dashboard:init_i18n(), Config. -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(), - ok; -set_special_configs(_) -> - ok. - end_per_suite(Config) -> end_suite(), Config. end_suite() -> application:unload(emqx_management), - emqx_common_test_helpers:stop_apps([emqx_dashboard]). + emqx_mgmt_api_test_util:end_suite([emqx_conf]). t_simple_binary(_config) -> Path = "/simple/bin", @@ -67,7 +59,7 @@ t_object(_config) -> <<"application/json">> => #{ <<"schema">> => #{ - required => [<<"timeout">>, <<"per_page">>], + required => [<<"per_page">>, <<"timeout">>], <<"properties">> => [ {<<"per_page">>, #{ description => <<"good per page desc">>, diff --git a/apps/emqx_gateway/src/emqx_gateway.app.src b/apps/emqx_gateway/src/emqx_gateway.app.src index 59eed7f3f..ced013497 100644 --- a/apps/emqx_gateway/src/emqx_gateway.app.src +++ b/apps/emqx_gateway/src/emqx_gateway.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_gateway, [ {description, "The Gateway management application"}, - {vsn, "0.1.13"}, + {vsn, "0.1.14"}, {registered, []}, {mod, {emqx_gateway_app, []}}, {applications, [kernel, stdlib, grpc, emqx, emqx_authn, emqx_ctl]}, diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 1c43340e2..62f723d59 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -180,7 +180,7 @@ schema("/gateways") -> #{ tags => ?TAGS, desc => ?DESC(list_gateway), - summary => <<"List All Gateways">>, + summary => <<"List all gateways">>, parameters => params_gateway_status_in_qs(), responses => #{ @@ -201,7 +201,7 @@ schema("/gateways/:name") -> #{ tags => ?TAGS, desc => ?DESC(get_gateway), - summary => <<"Get the Gateway">>, + summary => <<"Get gateway">>, parameters => params_gateway_name_in_path(), responses => #{ @@ -608,7 +608,7 @@ examples_gateway_confs() -> #{ stomp_gateway => #{ - summary => <<"A simple STOMP gateway configs">>, + summary => <<"A simple STOMP gateway config">>, value => #{ enable => true, @@ -636,7 +636,7 @@ examples_gateway_confs() -> }, mqttsn_gateway => #{ - summary => <<"A simple MQTT-SN gateway configs">>, + summary => <<"A simple MQTT-SN gateway config">>, value => #{ enable => true, @@ -672,7 +672,7 @@ examples_gateway_confs() -> }, coap_gateway => #{ - summary => <<"A simple CoAP gateway configs">>, + summary => <<"A simple CoAP gateway config">>, value => #{ enable => true, @@ -699,7 +699,7 @@ examples_gateway_confs() -> }, lwm2m_gateway => #{ - summary => <<"A simple LwM2M gateway configs">>, + summary => <<"A simple LwM2M gateway config">>, value => #{ enable => true, @@ -735,7 +735,7 @@ examples_gateway_confs() -> }, exproto_gateway => #{ - summary => <<"A simple ExProto gateway configs">>, + summary => <<"A simple ExProto gateway config">>, value => #{ enable => true, @@ -765,7 +765,7 @@ examples_update_gateway_confs() -> #{ stomp_gateway => #{ - summary => <<"A simple STOMP gateway configs">>, + summary => <<"A simple STOMP gateway config">>, value => #{ enable => true, @@ -782,7 +782,7 @@ examples_update_gateway_confs() -> }, mqttsn_gateway => #{ - summary => <<"A simple MQTT-SN gateway configs">>, + summary => <<"A simple MQTT-SN gateway config">>, value => #{ enable => true, @@ -803,7 +803,7 @@ examples_update_gateway_confs() -> }, coap_gateway => #{ - summary => <<"A simple CoAP gateway configs">>, + summary => <<"A simple CoAP gateway config">>, value => #{ enable => true, @@ -819,7 +819,7 @@ examples_update_gateway_confs() -> }, lwm2m_gateway => #{ - summary => <<"A simple LwM2M gateway configs">>, + summary => <<"A simple LwM2M gateway config">>, value => #{ enable => true, @@ -844,7 +844,7 @@ examples_update_gateway_confs() -> }, exproto_gateway => #{ - summary => <<"A simple ExProto gateway configs">>, + summary => <<"A simple ExProto gateway config">>, value => #{ enable => true, diff --git a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl index f52b26cd2..41b1b11d5 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl @@ -185,13 +185,13 @@ schema("/gateways/:name/authentication") -> #{ tags => ?TAGS, desc => ?DESC(get_authn), - summary => <<"Get Authenticator Configuration">>, + summary => <<"Get authenticator configuration">>, parameters => params_gateway_name_in_path(), responses => ?STANDARD_RESP( #{ 200 => schema_authn(), - 204 => <<"Authenticator doesn't initiated">> + 204 => <<"Authenticator not initialized">> } ) }, @@ -199,7 +199,7 @@ schema("/gateways/:name/authentication") -> #{ tags => ?TAGS, desc => ?DESC(update_authn), - summary => <<"Update Authenticator Configuration">>, + summary => <<"Update authenticator configuration">>, parameters => params_gateway_name_in_path(), 'requestBody' => schema_authn(), responses => @@ -209,7 +209,7 @@ schema("/gateways/:name/authentication") -> #{ tags => ?TAGS, desc => ?DESC(add_authn), - summary => <<"Create an Authenticator for a Gateway">>, + summary => <<"Create authenticator for gateway">>, parameters => params_gateway_name_in_path(), 'requestBody' => schema_authn(), responses => @@ -219,7 +219,7 @@ schema("/gateways/:name/authentication") -> #{ tags => ?TAGS, desc => ?DESC(delete_authn), - summary => <<"Delete the Gateway Authenticator">>, + summary => <<"Delete gateway authenticator">>, parameters => params_gateway_name_in_path(), responses => ?STANDARD_RESP(#{204 => <<"Deleted">>}) @@ -232,7 +232,7 @@ schema("/gateways/:name/authentication/users") -> #{ tags => ?TAGS, desc => ?DESC(list_users), - summary => <<"List users for a Gateway Authenticator">>, + summary => <<"List users for gateway authenticator">>, parameters => params_gateway_name_in_path() ++ params_paging_in_qs() ++ params_fuzzy_in_qs(), @@ -250,7 +250,7 @@ schema("/gateways/:name/authentication/users") -> #{ tags => ?TAGS, desc => ?DESC(add_user), - summary => <<"Add User for a Gateway Authenticator">>, + summary => <<"Add user for gateway authenticator">>, parameters => params_gateway_name_in_path(), 'requestBody' => emqx_dashboard_swagger:schema_with_examples( ref(emqx_authn_api, request_user_create), @@ -274,7 +274,7 @@ schema("/gateways/:name/authentication/users/:uid") -> #{ tags => ?TAGS, desc => ?DESC(get_user), - summary => <<"Get User Info for a Gateway Authenticator">>, + summary => <<"Get user info for gateway authenticator">>, parameters => params_gateway_name_in_path() ++ params_userid_in_path(), responses => @@ -291,7 +291,7 @@ schema("/gateways/:name/authentication/users/:uid") -> #{ tags => ?TAGS, desc => ?DESC(update_user), - summary => <<"Update User Info for a Gateway Authenticator">>, + summary => <<"Update user info for gateway authenticator">>, parameters => params_gateway_name_in_path() ++ params_userid_in_path(), 'requestBody' => emqx_dashboard_swagger:schema_with_examples( @@ -312,7 +312,7 @@ schema("/gateways/:name/authentication/users/:uid") -> #{ tags => ?TAGS, desc => ?DESC(delete_user), - summary => <<"Delete User for a Gateway Authenticator">>, + summary => <<"Delete user for gateway authenticator">>, parameters => params_gateway_name_in_path() ++ params_userid_in_path(), responses => diff --git a/apps/emqx_gateway/src/emqx_gateway_api_authn_user_import.erl b/apps/emqx_gateway/src/emqx_gateway_api_authn_user_import.erl index 705fccf90..68f392923 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_authn_user_import.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_authn_user_import.erl @@ -126,7 +126,7 @@ schema("/gateways/:name/authentication/import_users") -> #{ tags => ?TAGS, desc => ?DESC(emqx_gateway_api_authn, import_users), - summary => <<"Import Users">>, + summary => <<"Import users">>, parameters => params_gateway_name_in_path(), 'requestBody' => emqx_dashboard_swagger:file_schema(filename), responses => @@ -140,7 +140,7 @@ schema("/gateways/:name/listeners/:id/authentication/import_users") -> #{ tags => ?TAGS, desc => ?DESC(emqx_gateway_api_listeners, import_users), - summary => <<"Import Users">>, + summary => <<"Import users">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path(), 'requestBody' => emqx_dashboard_swagger:file_schema(filename), diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl index b30de3a3e..e64e918b4 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -460,7 +460,7 @@ schema("/gateways/:name/clients") -> #{ tags => ?TAGS, desc => ?DESC(list_clients), - summary => <<"List Gateway's Clients">>, + summary => <<"List gateway's clients">>, parameters => params_client_query(), responses => ?STANDARD_RESP(#{ @@ -478,7 +478,7 @@ schema("/gateways/:name/clients/:clientid") -> #{ tags => ?TAGS, desc => ?DESC(get_client), - summary => <<"Get Client Info">>, + summary => <<"Get client info">>, parameters => params_client_insta(), responses => ?STANDARD_RESP(#{200 => schema_client()}) @@ -487,7 +487,7 @@ schema("/gateways/:name/clients/:clientid") -> #{ tags => ?TAGS, desc => ?DESC(kick_client), - summary => <<"Kick out Client">>, + summary => <<"Kick out client">>, parameters => params_client_insta(), responses => ?STANDARD_RESP(#{204 => <<"Kicked">>}) @@ -500,7 +500,7 @@ schema("/gateways/:name/clients/:clientid/subscriptions") -> #{ tags => ?TAGS, desc => ?DESC(list_subscriptions), - summary => <<"List Client's Subscription">>, + summary => <<"List client's subscription">>, parameters => params_client_insta(), responses => ?STANDARD_RESP( @@ -516,7 +516,7 @@ schema("/gateways/:name/clients/:clientid/subscriptions") -> #{ tags => ?TAGS, desc => ?DESC(add_subscription), - summary => <<"Add Subscription for Client">>, + summary => <<"Add subscription for client">>, parameters => params_client_insta(), 'requestBody' => emqx_dashboard_swagger:schema_with_examples( ref(subscription), @@ -540,7 +540,7 @@ schema("/gateways/:name/clients/:clientid/subscriptions/:topic") -> #{ tags => ?TAGS, desc => ?DESC(delete_subscription), - summary => <<"Delete Client's Subscription">>, + summary => <<"Delete client's subscription">>, parameters => params_topic_name_in_path() ++ params_client_insta(), responses => ?STANDARD_RESP(#{204 => <<"Unsubscribed">>}) @@ -1020,12 +1020,12 @@ examples_client_list() -> #{ general_client_list => #{ - summary => <<"General Client List">>, + summary => <<"General client list">>, value => [example_general_client()] }, lwm2m_client_list => #{ - summary => <<"LwM2M Client List">>, + summary => <<"LwM2M client list">>, value => [example_lwm2m_client()] } }. @@ -1034,12 +1034,12 @@ examples_client() -> #{ general_client => #{ - summary => <<"General Client Info">>, + summary => <<"General client info">>, value => example_general_client() }, lwm2m_client => #{ - summary => <<"LwM2M Client Info">>, + summary => <<"LwM2M client info">>, value => example_lwm2m_client() } }. @@ -1048,12 +1048,12 @@ examples_subscription_list() -> #{ general_subscription_list => #{ - summary => <<"A General Subscription List">>, + summary => <<"A general subscription list">>, value => [example_general_subscription()] }, stomp_subscription_list => #{ - summary => <<"The Stomp Subscription List">>, + summary => <<"The STOMP subscription list">>, value => [example_stomp_subscription] } }. @@ -1062,12 +1062,12 @@ examples_subscription() -> #{ general_subscription => #{ - summary => <<"A General Subscription">>, + summary => <<"A general subscription">>, value => example_general_subscription() }, stomp_subscription => #{ - summary => <<"A Stomp Subscription">>, + summary => <<"A STOMP subscription">>, value => example_stomp_subscription() } }. diff --git a/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl b/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl index 43c8156d6..14b80a500 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl @@ -362,7 +362,7 @@ schema("/gateways/:name/listeners") -> #{ tags => ?TAGS, desc => ?DESC(list_listeners), - summary => <<"List All Listeners">>, + summary => <<"List all listeners">>, parameters => params_gateway_name_in_path(), responses => ?STANDARD_RESP( @@ -378,7 +378,7 @@ schema("/gateways/:name/listeners") -> #{ tags => ?TAGS, desc => ?DESC(add_listener), - summary => <<"Add a Listener">>, + summary => <<"Add listener">>, parameters => params_gateway_name_in_path(), %% XXX: How to distinguish the different listener supported by %% different types of gateways? @@ -404,7 +404,7 @@ schema("/gateways/:name/listeners/:id") -> #{ tags => ?TAGS, desc => ?DESC(get_listener), - summary => <<"Get the Listener Configs">>, + summary => <<"Get listener config">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path(), responses => @@ -421,7 +421,7 @@ schema("/gateways/:name/listeners/:id") -> #{ tags => ?TAGS, desc => ?DESC(delete_listener), - summary => <<"Delete the Listener">>, + summary => <<"Delete listener">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path(), responses => @@ -431,7 +431,7 @@ schema("/gateways/:name/listeners/:id") -> #{ tags => ?TAGS, desc => ?DESC(update_listener), - summary => <<"Update the Listener Configs">>, + summary => <<"Update listener config">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path(), 'requestBody' => emqx_dashboard_swagger:schema_with_examples( @@ -456,7 +456,7 @@ schema("/gateways/:name/listeners/:id/authentication") -> #{ tags => ?TAGS, desc => ?DESC(get_listener_authn), - summary => <<"Get the Listener's Authenticator">>, + summary => <<"Get the listener's authenticator">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path(), responses => @@ -471,7 +471,7 @@ schema("/gateways/:name/listeners/:id/authentication") -> #{ tags => ?TAGS, desc => ?DESC(add_listener_authn), - summary => <<"Create an Authenticator for a Listener">>, + summary => <<"Create authenticator for listener">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path(), 'requestBody' => schema_authn(), @@ -482,7 +482,7 @@ schema("/gateways/:name/listeners/:id/authentication") -> #{ tags => ?TAGS, desc => ?DESC(update_listener_authn), - summary => <<"Update the Listener Authenticator configs">>, + summary => <<"Update config of authenticator for listener">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path(), 'requestBody' => schema_authn(), @@ -493,7 +493,7 @@ schema("/gateways/:name/listeners/:id/authentication") -> #{ tags => ?TAGS, desc => ?DESC(delete_listener_authn), - summary => <<"Delete the Listener's Authenticator">>, + summary => <<"Delete the listener's authenticator">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path(), responses => @@ -507,7 +507,7 @@ schema("/gateways/:name/listeners/:id/authentication/users") -> #{ tags => ?TAGS, desc => ?DESC(list_users), - summary => <<"List Authenticator's Users">>, + summary => <<"List authenticator's users">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path() ++ params_paging_in_qs(), @@ -525,7 +525,7 @@ schema("/gateways/:name/listeners/:id/authentication/users") -> #{ tags => ?TAGS, desc => ?DESC(add_user), - summary => <<"Add User for an Authenticator">>, + summary => <<"Add user for an authenticator">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path(), 'requestBody' => emqx_dashboard_swagger:schema_with_examples( @@ -550,7 +550,7 @@ schema("/gateways/:name/listeners/:id/authentication/users/:uid") -> #{ tags => ?TAGS, desc => ?DESC(get_user), - summary => <<"Get User Info">>, + summary => <<"Get user info">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path() ++ params_userid_in_path(), @@ -568,7 +568,7 @@ schema("/gateways/:name/listeners/:id/authentication/users/:uid") -> #{ tags => ?TAGS, desc => ?DESC(update_user), - summary => <<"Update User Info">>, + summary => <<"Update user info">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path() ++ params_userid_in_path(), @@ -590,7 +590,7 @@ schema("/gateways/:name/listeners/:id/authentication/users/:uid") -> #{ tags => ?TAGS, desc => ?DESC(delete_user), - summary => <<"Delete User">>, + summary => <<"Delete user">>, parameters => params_gateway_name_in_path() ++ params_listener_id_in_path() ++ params_userid_in_path(), @@ -712,7 +712,7 @@ examples_listener() -> #{ tcp_listener => #{ - summary => <<"A simple tcp listener example">>, + summary => <<"A simple TCP listener example">>, value => #{ name => <<"tcp-def">>, @@ -738,7 +738,7 @@ examples_listener() -> }, ssl_listener => #{ - summary => <<"A simple ssl listener example">>, + summary => <<"A simple SSL listener example">>, value => #{ name => <<"ssl-def">>, @@ -771,7 +771,7 @@ examples_listener() -> }, udp_listener => #{ - summary => <<"A simple udp listener example">>, + summary => <<"A simple UDP listener example">>, value => #{ name => <<"udp-def">>, @@ -789,7 +789,7 @@ examples_listener() -> }, dtls_listener => #{ - summary => <<"A simple dtls listener example">>, + summary => <<"A simple DTLS listener example">>, value => #{ name => <<"dtls-def">>, @@ -817,7 +817,7 @@ examples_listener() -> }, dtls_listener_with_psk_ciphers => #{ - summary => <<"A dtls listener with PSK example">>, + summary => <<"A DTLS listener with PSK example">>, value => #{ name => <<"dtls-psk">>, @@ -845,7 +845,7 @@ examples_listener() -> }, lisetner_with_authn => #{ - summary => <<"A tcp listener with authentication example">>, + summary => <<"A TCP listener with authentication example">>, value => #{ name => <<"tcp-with-authn">>, diff --git a/apps/emqx_gateway/src/emqx_gateway_cm.erl b/apps/emqx_gateway/src/emqx_gateway_cm.erl index 599493d97..71ec4bf59 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm.erl @@ -587,24 +587,24 @@ request_stepdown(Action, ConnMod, Pid) -> catch % emqx_ws_connection: call _:noproc -> - ok = ?tp(debug, "session_already_gone", #{pid => Pid, action => Action}), + ok = ?tp(debug, "session_already_gone", #{stale_pid => Pid, action => Action}), {error, noproc}; % emqx_connection: gen_server:call _:{noproc, _} -> - ok = ?tp(debug, "session_already_gone", #{pid => Pid, action => Action}), + ok = ?tp(debug, "session_already_gone", #{stale_pid => Pid, action => Action}), {error, noproc}; _:Reason = {shutdown, _} -> - ok = ?tp(debug, "session_already_shutdown", #{pid => Pid, action => Action}), + ok = ?tp(debug, "session_already_shutdown", #{stale_pid => Pid, action => Action}), {error, Reason}; _:Reason = {{shutdown, _}, _} -> - ok = ?tp(debug, "session_already_shutdown", #{pid => Pid, action => Action}), + ok = ?tp(debug, "session_already_shutdown", #{stale_pid => Pid, action => Action}), {error, Reason}; _:{timeout, {gen_server, call, _}} -> ?tp( warning, "session_stepdown_request_timeout", #{ - pid => Pid, + stale_pid => Pid, action => Action, stale_channel => stale_channel_info(Pid) } @@ -616,7 +616,7 @@ request_stepdown(Action, ConnMod, Pid) -> error, "session_stepdown_request_exception", #{ - pid => Pid, + stale_pid => Pid, action => Action, reason => Error, stacktrace => St, diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 2034a40eb..741fb98ae 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -453,20 +453,20 @@ fields(translator) -> ]; fields(udp_listeners) -> [ - {udp, sc(map(name, ref(udp_listener)), #{desc => ?DESC(udp_listener)})}, - {dtls, sc(map(name, ref(dtls_listener)), #{desc => ?DESC(dtls_listener)})} + {udp, sc(map(name, ref(udp_listener)), #{desc => ?DESC(listener_name_to_settings_map)})}, + {dtls, sc(map(name, ref(dtls_listener)), #{desc => ?DESC(listener_name_to_settings_map)})} ]; fields(tcp_listeners) -> [ - {tcp, sc(map(name, ref(tcp_listener)), #{desc => ?DESC(tcp_listener)})}, - {ssl, sc(map(name, ref(ssl_listener)), #{desc => ?DESC(ssl_listener)})} + {tcp, sc(map(name, ref(tcp_listener)), #{desc => ?DESC(listener_name_to_settings_map)})}, + {ssl, sc(map(name, ref(ssl_listener)), #{desc => ?DESC(listener_name_to_settings_map)})} ]; fields(tcp_udp_listeners) -> [ - {tcp, sc(map(name, ref(tcp_listener)), #{desc => ?DESC(tcp_listener)})}, - {ssl, sc(map(name, ref(ssl_listener)), #{desc => ?DESC(ssl_listener)})}, - {udp, sc(map(name, ref(udp_listener)), #{desc => ?DESC(udp_listener)})}, - {dtls, sc(map(name, ref(dtls_listener)), #{desc => ?DESC(dtls_listener)})} + {tcp, sc(map(name, ref(tcp_listener)), #{desc => ?DESC(listener_name_to_settings_map)})}, + {ssl, sc(map(name, ref(ssl_listener)), #{desc => ?DESC(listener_name_to_settings_map)})}, + {udp, sc(map(name, ref(udp_listener)), #{desc => ?DESC(listener_name_to_settings_map)})}, + {dtls, sc(map(name, ref(dtls_listener)), #{desc => ?DESC(listener_name_to_settings_map)})} ]; fields(tcp_listener) -> %% some special configs for tcp listener @@ -558,19 +558,19 @@ desc(udp_listeners) -> desc(tcp_listeners) -> "Settings for the TCP listeners."; desc(tcp_udp_listeners) -> - "Settings for the listeners."; + "Settings for TCP and UDP listeners."; desc(tcp_listener) -> - "Settings for the TCP listener."; + "Settings for TCP listener."; desc(ssl_listener) -> - "Settings for the SSL listener."; + "Settings for SSL listener."; desc(udp_listener) -> - "Settings for the UDP listener."; + "Settings for UDP listener."; desc(dtls_listener) -> - "Settings for the DTLS listener."; + "Settings for DTLS listener."; desc(udp_opts) -> - "Settings for the UDP sockets."; + "Settings for UDP sockets."; desc(dtls_opts) -> - "Settings for the DTLS protocol."; + "Settings for DTLS protocol."; desc(_) -> undefined. @@ -625,7 +625,7 @@ mountpoint(Default) -> binary(), #{ default => iolist_to_binary(Default), - desc => ?DESC(gateway_common_mountpoint) + desc => ?DESC(gateway_mountpoint) } ). @@ -674,7 +674,7 @@ common_listener_opts() -> binary(), #{ default => undefined, - desc => ?DESC(gateway_common_listener_mountpoint) + desc => ?DESC(gateway_mountpoint) } )}, {access_rules, diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl index d0b362dda..090af3e87 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl @@ -138,7 +138,7 @@ mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"discover">>, <<"dat [ {uri_path, FullPathList}, {uri_query, QueryList}, - {'accept', ?LWM2M_FORMAT_LINK} + {accept, ?LWM2M_FORMAT_LINK} ] ), InputCmd @@ -241,6 +241,7 @@ empty_ack_to_mqtt(Ref) -> coap_failure_to_mqtt(Ref, MsgType) -> make_base_response(maps:put(<<"msgType">>, MsgType, Ref)). +%% TODO: application/link-format content_to_mqtt(CoapPayload, <<"text/plain">>, Ref) -> emqx_lwm2m_message:text_to_json(extract_path(Ref), CoapPayload); content_to_mqtt(CoapPayload, <<"application/octet-stream">>, Ref) -> diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl index 19cd5c25d..8634280e3 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl @@ -15,11 +15,12 @@ %%-------------------------------------------------------------------- -module(emqx_lwm2m_session). +-include("src/coap/include/emqx_coap.hrl"). +-include("src/lwm2m/include/emqx_lwm2m.hrl"). -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). --include("src/coap/include/emqx_coap.hrl"). --include("src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). %% API -export([ @@ -513,12 +514,20 @@ observe_object_list(AlternatePath, ObjectList, Session) -> true -> Acc; false -> - try - emqx_lwm2m_xml_object_db:find_objectid(binary_to_integer(ObjId)), - observe_object(AlternatePath, ObjectPath, Acc) - catch - error:no_xml_definition -> - Acc + ObjId1 = binary_to_integer(ObjId), + case emqx_lwm2m_xml_object_db:find_objectid(ObjId1) of + {error, no_xml_definition} -> + ?tp( + warning, + ignore_observer_resource, + #{ + reason => no_xml_definition, + object_id => ObjId1 + } + ), + Acc; + _ -> + observe_object(AlternatePath, ObjectPath, Acc) end end end, @@ -538,15 +547,20 @@ deliver_auto_observe_to_coap(AlternatePath, TermData, Session) -> path => AlternatePath, data => TermData }), - {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), + {Req0, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), + Req = alloc_token(Req0), maybe_do_deliver_to_coap(Ctx, Req, 0, false, Session). is_auto_observe() -> emqx:get_config([gateway, lwm2m, auto_observe]). +alloc_token(Req = #coap_message{}) -> + Req#coap_message{token = crypto:strong_rand_bytes(4)}. + %%-------------------------------------------------------------------- %% Response %%-------------------------------------------------------------------- + handle_coap_response( {Ctx = #{<<"msgType">> := EventType}, #coap_message{ method = CoapMsgMethod, diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl index 19335768f..58373e114 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl @@ -57,6 +57,7 @@ start_link(XmlDir) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [XmlDir], []). +-spec find_objectid(integer()) -> {error, no_xml_definition} | xmerl:xmlElement(). find_objectid(ObjectId) -> ObjectIdInt = case is_list(ObjectId) of @@ -65,9 +66,10 @@ find_objectid(ObjectId) -> end, case ets:lookup(?LWM2M_OBJECT_DEF_TAB, ObjectIdInt) of [] -> {error, no_xml_definition}; - [{ObjectId, Xml}] -> Xml + [{_ObjectId, Xml}] -> Xml end. +-spec find_name(string()) -> {error, no_xml_definition} | xmerl:xmlElement(). find_name(Name) -> NameBinary = case is_list(Name) of @@ -77,10 +79,11 @@ find_name(Name) -> case ets:lookup(?LWM2M_OBJECT_NAME_TO_ID_TAB, NameBinary) of [] -> {error, no_xml_definition}; - [{NameBinary, ObjectId}] -> + [{_NameBinary, ObjectId}] -> find_objectid(ObjectId) end. +-spec stop() -> ok. stop() -> gen_server:stop(?MODULE). diff --git a/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl b/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl index 1f02a1637..e1a6ec0d6 100644 --- a/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl +++ b/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl @@ -36,8 +36,39 @@ -define(ERR_BAD_REQUEST, <<"Bad Request">>). -define(REG_PREFIX, <<"rd">>). +%%-------------------------------------------------------------------- +%% Data formats for transferring resource information, defined in +%% OMA-TS-LightweightM2M-V1_0_1-20170704-A + +%% 0: Plain text. 0 is numeric value used in CoAP Content-Format option. +%% The plain text format is used for "Read" and "Write" operations on singular +%% Resources. i.e: /3/0/0 +%% +%% This data format has a Media Type of "text/plain". -define(LWM2M_FORMAT_PLAIN_TEXT, 0). + +%% 40: Link format. 40 is numeric value used in CoAP Content-Format option. +%% -define(LWM2M_FORMAT_LINK, 40). + +%% 42: Opaque. 41 is numeric value used in CoAP Content-Format option. +%% The opaque format is used for "Read" and "Write" operations on singular +%% Resources where the value of the Resource is an opaque binary value. +%% i.e: firmware images or opaque value from top layer. +%% +%% This data format has a Media Type of "application/octet-stream". -define(LWM2M_FORMAT_OPAQUE, 42). + +%% 11542: TLV. 11542 is numeric value used in CoAP Content-Format option. +%% For "Read" and "Write" operation, the binary TLV format is used to represent +%% an array of values or a single value using a compact binary representation. +%% +%% This data format has a Media Type of "application/vnd.oma.lwm2m+tlv". -define(LWM2M_FORMAT_TLV, 11542). --define(LWMWM_FORMAT_JSON, 11543). + +%% 11543: JSON. 11543 is numeric value used in CoAP Content-Format option. +%% The client may support the JSON format for "Read" and "Write" operations to +%% represent multiple resource or single resource values. +%% +%% This data format has a Media Type of "application/vnd.oma.lwm2m+json". +-define(LWM2M_FORMAT_OMA_JSON, 11543). diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index f91bbf16e..fc852709c 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -35,29 +35,7 @@ -include("src/coap/include/emqx_coap.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). - --define(CONF_DEFAULT, << - "\n" - "gateway.lwm2m {\n" - " xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\"\n" - " lifetime_min = 1s\n" - " lifetime_max = 86400s\n" - " qmode_time_window = 22\n" - " auto_observe = false\n" - " mountpoint = \"lwm2m/${username}\"\n" - " update_msg_publish_condition = contains_object_list\n" - " translators {\n" - " command = {topic = \"/dn/#\", qos = 0}\n" - " response = {topic = \"/up/resp\", qos = 0}\n" - " notify = {topic = \"/up/notify\", qos = 0}\n" - " register = {topic = \"/up/resp\", qos = 0}\n" - " update = {topic = \"/up/resp\", qos = 0}\n" - " }\n" - " listeners.udp.default {\n" - " bind = 5783\n" - " }\n" - "}\n" ->>). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -record(coap_content, {content_format, payload = <<>>}). @@ -99,7 +77,8 @@ groups() -> %% case06_register_wrong_lifetime, %% now, will ignore wrong lifetime case07_register_alternate_path_01, case07_register_alternate_path_02, - case08_reregister + case08_reregister, + case09_auto_observe ]}, {test_grp_1_read, [RepeatOpt], [ case10_read, @@ -164,8 +143,15 @@ end_per_suite(Config) -> emqx_mgmt_api_test_util:end_suite([emqx_conf, emqx_authn]), Config. -init_per_testcase(_AllTestCase, Config) -> - ok = emqx_common_test_helpers:load_config(emqx_gateway_schema, ?CONF_DEFAULT), +init_per_testcase(TestCase, Config) -> + GatewayConfig = + case TestCase of + case09_auto_observe -> + default_config(#{auto_observe => true}); + _ -> + default_config() + end, + ok = emqx_common_test_helpers:load_config(emqx_gateway_schema, GatewayConfig), {ok, _} = application:ensure_all_started(emqx_gateway), {ok, ClientUdpSock} = gen_udp:open(0, [binary, {active, false}]), @@ -187,7 +173,37 @@ end_per_testcase(_AllTestCase, Config) -> ok = application:stop(emqx_gateway). default_config() -> - ?CONF_DEFAULT. + default_config(#{}). + +default_config(Overrides) -> + iolist_to_binary( + io_lib:format( + "\n" + "gateway.lwm2m {\n" + " xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\"\n" + " lifetime_min = 1s\n" + " lifetime_max = 86400s\n" + " qmode_time_window = 22\n" + " auto_observe = ~w\n" + " mountpoint = \"lwm2m/${username}\"\n" + " update_msg_publish_condition = contains_object_list\n" + " translators {\n" + " command = {topic = \"/dn/#\", qos = 0}\n" + " response = {topic = \"/up/resp\", qos = 0}\n" + " notify = {topic = \"/up/notify\", qos = 0}\n" + " register = {topic = \"/up/resp\", qos = 0}\n" + " update = {topic = \"/up/resp\", qos = 0}\n" + " }\n" + " listeners.udp.default {\n" + " bind = ~w\n" + " }\n" + "}\n", + [ + maps:get(auto_observe, Overrides, false), + maps:get(bind, Overrides, ?PORT) + ] + ) + ). default_port() -> ?PORT. @@ -762,6 +778,52 @@ case08_reregister(Config) -> %% verify the lwm2m client is still online ?assertEqual(ReadResult, test_recv_mqtt_response(ReportTopic)). +case09_auto_observe(Config) -> + UdpSock = ?config(sock, Config), + Epn = "urn:oma:lwm2m:oma:3", + MsgId1 = 15, + RespTopic = list_to_binary("lwm2m/" ++ Epn ++ "/up/resp"), + emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), + timer:sleep(200), + + ok = snabbkaffe:start_trace(), + + %% step 1, device register ... + test_send_coap_request( + UdpSock, + post, + sprintf("coap://127.0.0.1:~b/rd?ep=~ts<=345&lwm2m=1", [?PORT, Epn]), + #coap_content{ + content_format = <<"text/plain">>, + payload = << + ";rt=\"oma.lwm2m\";ct=11543," + ",,," + >> + }, + [], + MsgId1 + ), + #coap_message{method = Method1} = test_recv_coap_response(UdpSock), + ?assertEqual({ok, created}, Method1), + + #coap_message{ + method = Method2, + token = Token2, + options = Options2 + } = test_recv_coap_request(UdpSock), + ?assertEqual(get, Method2), + ?assertNotEqual(<<>>, Token2), + ?assertMatch( + #{ + observe := 0, + uri_path := [<<"lwm2m">>, <<"3">>, <<"0">>] + }, + Options2 + ), + + {ok, _} = ?block_until(#{?snk_kind := ignore_observer_resource}, 1000), + ok. + case10_read(Config) -> UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", diff --git a/apps/emqx_management/src/emqx_management.app.src b/apps/emqx_management/src/emqx_management.app.src index 966358f47..9863f5cf6 100644 --- a/apps/emqx_management/src/emqx_management.app.src +++ b/apps/emqx_management/src/emqx_management.app.src @@ -2,7 +2,7 @@ {application, emqx_management, [ {description, "EMQX Management API and CLI"}, % strict semver, bump manually! - {vsn, "5.0.16"}, + {vsn, "5.0.17"}, {modules, []}, {registered, [emqx_management_sup]}, {applications, [kernel, stdlib, emqx_plugins, minirest, emqx, emqx_ctl]}, diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 2e6aac849..55cc50597 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -119,6 +119,7 @@ schema("/configs_reset/:rootname") -> "- For a config entry that has default value, this resets it to the default value;\n" "- For a config entry that has no default value, an error 400 will be returned" >>, + summary => <<"Reset config entry">>, %% We only return "200" rather than the new configs that has been changed, as %% the schema of the changed configs is depends on the request parameter %% `conf_path`, it cannot be defined here. diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index 21d905331..a4173f5b0 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -17,7 +17,6 @@ -behaviour(minirest_api). --include_lib("emqx/include/emqx.hrl"). -include_lib("typerefl/include/types.hrl"). -import(hoconsc, [mk/2, ref/1, ref/2, enum/1, array/1]). @@ -25,8 +24,6 @@ -define(NODE_METRICS_MODULE, emqx_mgmt_api_metrics). -define(NODE_STATS_MODULE, emqx_mgmt_api_stats). --define(SOURCE_ERROR, 'SOURCE_ERROR'). - %% Swagger specs from hocon schema -export([ api_spec/0, @@ -88,7 +85,7 @@ schema("/nodes/:node") -> ref(node_info), #{desc => <<"Get node info successfully">>} ), - 400 => node_error() + 404 => not_found() } } }; @@ -106,7 +103,7 @@ schema("/nodes/:node/metrics") -> ref(?NODE_METRICS_MODULE, node_metrics), #{desc => <<"Get node metrics successfully">>} ), - 400 => node_error() + 404 => not_found() } } }; @@ -124,7 +121,7 @@ schema("/nodes/:node/stats") -> ref(?NODE_STATS_MODULE, node_stats_data), #{desc => <<"Get node stats successfully">>} ), - 400 => node_error() + 404 => not_found() } } }. @@ -136,7 +133,7 @@ fields(node_name) -> [ {node, mk( - atom(), + binary(), #{ in => path, description => <<"Node name">>, @@ -250,55 +247,46 @@ nodes(get, _Params) -> list_nodes(#{}). node(get, #{bindings := #{node := NodeName}}) -> - get_node(NodeName). + emqx_api_lib:with_node(NodeName, to_ok_result_fun(fun get_node/1)). node_metrics(get, #{bindings := #{node := NodeName}}) -> - get_metrics(NodeName). + emqx_api_lib:with_node(NodeName, to_ok_result_fun(fun emqx_mgmt:get_metrics/1)). node_stats(get, #{bindings := #{node := NodeName}}) -> - get_stats(NodeName). + emqx_api_lib:with_node(NodeName, to_ok_result_fun(fun emqx_mgmt:get_stats/1)). %%-------------------------------------------------------------------- %% api apply list_nodes(#{}) -> - NodesInfo = [format(Node, NodeInfo) || {Node, NodeInfo} <- emqx_mgmt:list_nodes()], + NodesInfo = [format(NodeInfo) || {_Node, NodeInfo} <- emqx_mgmt:list_nodes()], {200, NodesInfo}. get_node(Node) -> - case emqx_mgmt:lookup_node(Node) of - {error, _} -> - {400, #{code => 'SOURCE_ERROR', message => <<"rpc_failed">>}}; - NodeInfo -> - {200, format(Node, NodeInfo)} - end. - -get_metrics(Node) -> - case emqx_mgmt:get_metrics(Node) of - {error, _} -> - {400, #{code => 'SOURCE_ERROR', message => <<"rpc_failed">>}}; - Metrics -> - {200, Metrics} - end. - -get_stats(Node) -> - case emqx_mgmt:get_stats(Node) of - {error, _} -> - {400, #{code => 'SOURCE_ERROR', message => <<"rpc_failed">>}}; - Stats -> - {200, Stats} - end. + format(emqx_mgmt:lookup_node(Node)). %%-------------------------------------------------------------------- %% internal function -format(_Node, Info = #{memory_total := Total, memory_used := Used}) -> +format(Info = #{memory_total := Total, memory_used := Used}) -> Info#{ memory_total := emqx_mgmt_util:kmg(Total), memory_used := emqx_mgmt_util:kmg(Used) }; -format(_Node, Info) when is_map(Info) -> +format(Info) when is_map(Info) -> Info. -node_error() -> - emqx_dashboard_swagger:error_codes([?SOURCE_ERROR], <<"Node error">>). +to_ok_result({error, _} = Error) -> + Error; +to_ok_result({ok, _} = Ok) -> + Ok; +to_ok_result(Result) -> + {ok, Result}. + +to_ok_result_fun(Fun) when is_function(Fun) -> + fun(Arg) -> + to_ok_result(Fun(Arg)) + end. + +not_found() -> + emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"Node not found">>). diff --git a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl index 4930e587c..92814d112 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl @@ -48,6 +48,9 @@ -define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$"). -define(TAGS, [<<"Plugins">>]). +%% Plugin NameVsn must follow the pattern -, +%% app_name must be a snake_case (no '-' allowed). +-define(VSN_WILDCARD, "-*.tar.gz"). namespace() -> "plugins". @@ -68,10 +71,10 @@ schema("/plugins") -> #{ 'operationId' => list_plugins, get => #{ + summary => <<"List all installed plugins">>, description => - "List all install plugins.
" "Plugins are launched in top-down order.
" - "Using `POST /plugins/{name}/move` to change the boot order.", + "Use `POST /plugins/{name}/move` to change the boot order.", tags => ?TAGS, responses => #{ 200 => hoconsc:array(hoconsc:ref(plugin)) @@ -82,8 +85,9 @@ schema("/plugins/install") -> #{ 'operationId' => upload_install, post => #{ + summary => <<"Install a new plugin">>, description => - "Install a plugin(plugin-vsn.tar.gz)." + "Upload a plugin tarball (plugin-vsn.tar.gz)." "Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) " "to develop plugin.", tags => ?TAGS, @@ -112,7 +116,8 @@ schema("/plugins/:name") -> #{ 'operationId' => plugin, get => #{ - description => "Describe a plugin according `release.json` and `README.md`.", + summary => <<"Get a plugin description">>, + description => "Describs plugin according to its `release.json` and `README.md`.", tags => ?TAGS, parameters => [hoconsc:ref(name)], responses => #{ @@ -121,7 +126,8 @@ schema("/plugins/:name") -> } }, delete => #{ - description => "Uninstall a plugin package.", + summary => <<"Delete a plugin">>, + description => "Uninstalls a previously uploaded plugin package.", tags => ?TAGS, parameters => [hoconsc:ref(name)], responses => #{ @@ -134,6 +140,7 @@ schema("/plugins/:name/:action") -> #{ 'operationId' => update_plugin, put => #{ + summary => <<"Trigger action on an installed plugin">>, description => "start/stop a installed plugin.
" "- **start**: start the plugin.
" @@ -153,6 +160,7 @@ schema("/plugins/:name/move") -> #{ 'operationId' => update_boot_order, post => #{ + summary => <<"Move plugin within plugin hiearchy">>, description => "Setting the boot order of plugins.", tags => ?TAGS, parameters => [hoconsc:ref(name)], @@ -329,7 +337,7 @@ upload_install(post, #{body := #{<<"plugin">> := Plugin}}) when is_map(Plugin) - case emqx_plugins:parse_name_vsn(FileName) of {ok, AppName, _Vsn} -> AppDir = filename:join(emqx_plugins:install_dir(), AppName), - case filelib:wildcard(AppDir ++ "*.tar.gz") of + case filelib:wildcard(AppDir ++ ?VSN_WILDCARD) of [] -> do_install_package(FileName, Bin); OtherVsn -> @@ -420,6 +428,7 @@ update_boot_order(post, #{bindings := #{name := Name}, body := Body}) -> %% For RPC upload_install/2 install_package(FileName, Bin) -> File = filename:join(emqx_plugins:install_dir(), FileName), + ok = filelib:ensure_dir(File), ok = file:write_file(File, Bin), PackageName = string:trim(FileName, trailing, ".tar.gz"), case emqx_plugins:ensure_installed(PackageName) of diff --git a/apps/emqx_management/src/emqx_mgmt_api_publish.erl b/apps/emqx_management/src/emqx_mgmt_api_publish.erl index 245b56c1d..ba486ab89 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_publish.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_publish.erl @@ -50,6 +50,7 @@ schema("/publish") -> #{ 'operationId' => publish, post => #{ + summary => <<"Publish a message">>, description => ?DESC(publish_api), tags => [<<"Publish">>], 'requestBody' => hoconsc:mk(hoconsc:ref(?MODULE, publish_message)), @@ -65,6 +66,7 @@ schema("/publish/bulk") -> #{ 'operationId' => publish_batch, post => #{ + summary => <<"Publish a batch of messages">>, description => ?DESC(publish_bulk_api), tags => [<<"Publish">>], 'requestBody' => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, publish_message)), #{}), diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index bf84d03d5..409af4d95 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -177,7 +177,7 @@ format(WhichNode, {{Topic, _Subscriber}, Options}) -> maps:merge( #{ topic => get_topic(Topic, Options), - clientid => maps:get(subid, Options), + clientid => maps:get(subid, Options, null), node => WhichNode }, maps:with([qos, nl, rap, rh], Options) diff --git a/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl index 03b0ea2d9..30313e555 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl @@ -68,7 +68,7 @@ t_nodes_api(_) -> BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode"]), ?assertMatch( - {error, {_, 400, _}}, + {error, {_, 404, _}}, emqx_mgmt_api_test_util:request_api(get, BadNodePath) ). @@ -94,7 +94,7 @@ t_node_stats_api(_) -> BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "stats"]), ?assertMatch( - {error, {_, 400, _}}, + {error, {_, 404, _}}, emqx_mgmt_api_test_util:request_api(get, BadNodePath) ). @@ -112,7 +112,7 @@ t_node_metrics_api(_) -> BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "metrics"]), ?assertMatch( - {error, {_, 400, _}}, + {error, {_, 404, _}}, emqx_mgmt_api_test_util:request_api(get, BadNodePath) ). diff --git a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl index 0cf15d678..24e55494d 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl @@ -20,6 +20,7 @@ -include_lib("eunit/include/eunit.hrl"). +-define(EMQX_PLUGIN_TEMPLATE_NAME, "emqx_plugin_template"). -define(EMQX_PLUGIN_TEMPLATE_VSN, "5.0.0"). -define(PACKAGE_SUFFIX, ".tar.gz"). @@ -89,6 +90,27 @@ t_plugins(Config) -> {ok, []} = uninstall_plugin(NameVsn), ok. +t_install_plugin_matching_exisiting_name(Config) -> + DemoShDir = proplists:get_value(demo_sh_dir, Config), + PackagePath = get_demo_plugin_package(DemoShDir), + NameVsn = filename:basename(PackagePath, ?PACKAGE_SUFFIX), + ok = emqx_plugins:ensure_uninstalled(NameVsn), + ok = emqx_plugins:delete_package(NameVsn), + NameVsn1 = ?EMQX_PLUGIN_TEMPLATE_NAME ++ "_a" ++ "-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN, + PackagePath1 = create_renamed_package(PackagePath, NameVsn1), + NameVsn1 = filename:basename(PackagePath1, ?PACKAGE_SUFFIX), + ok = emqx_plugins:ensure_uninstalled(NameVsn1), + ok = emqx_plugins:delete_package(NameVsn1), + %% First, install plugin "emqx_plugin_template_a", then: + %% "emqx_plugin_template" which matches the beginning + %% of the previously installed plugin name + ok = install_plugin(PackagePath1), + ok = install_plugin(PackagePath), + {ok, _} = describe_plugins(NameVsn), + {ok, _} = describe_plugins(NameVsn1), + {ok, _} = uninstall_plugin(NameVsn), + {ok, _} = uninstall_plugin(NameVsn1). + t_bad_plugin(Config) -> DemoShDir = proplists:get_value(demo_sh_dir, Config), PackagePathOrig = get_demo_plugin_package(DemoShDir), @@ -160,9 +182,31 @@ uninstall_plugin(Name) -> get_demo_plugin_package(Dir) -> #{package := Pkg} = emqx_plugins_SUITE:get_demo_plugin_package(), - FileName = "emqx_plugin_template-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX, + FileName = ?EMQX_PLUGIN_TEMPLATE_NAME ++ "-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX, PluginPath = "./" ++ FileName, Pkg = filename:join([Dir, FileName]), _ = os:cmd("cp " ++ Pkg ++ " " ++ PluginPath), true = filelib:is_regular(PluginPath), PluginPath. + +create_renamed_package(PackagePath, NewNameVsn) -> + {ok, Content} = erl_tar:extract(PackagePath, [compressed, memory]), + {ok, NewName, _Vsn} = emqx_plugins:parse_name_vsn(NewNameVsn), + NewNameB = atom_to_binary(NewName, utf8), + Content1 = lists:map( + fun({F, B}) -> + [_ | PathPart] = filename:split(F), + B1 = update_release_json(PathPart, B, NewNameB), + {filename:join([NewNameVsn | PathPart]), B1} + end, + Content + ), + NewPackagePath = filename:join(filename:dirname(PackagePath), NewNameVsn ++ ?PACKAGE_SUFFIX), + ok = erl_tar:create(NewPackagePath, Content1, [compressed]), + NewPackagePath. + +update_release_json(["release.json"], FileContent, NewName) -> + ContentMap = emqx_json:decode(FileContent, [return_maps]), + emqx_json:encode(ContentMap#{<<"name">> => NewName}); +update_release_json(_FileName, FileContent, _NewName) -> + FileContent. diff --git a/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl index ccfa30037..b9e9fffd8 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl @@ -19,6 +19,7 @@ -compile(nowarn_export_all). -include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). -define(CLIENTID, <<"api_clientid">>). -define(USERNAME, <<"api_username">>). @@ -142,6 +143,18 @@ t_subscription_fuzzy_search(Config) -> ?assertEqual(#{<<"page">> => 2, <<"limit">> => 3, <<"hasnext">> => false}, MatchMeta2P2), ?assertEqual(1, length(maps:get(<<"data">>, MatchData2P2))). +%% checks that we can list when there are subscriptions made by +%% `emqx:subscribe'. +t_list_with_internal_subscription(_Config) -> + emqx:subscribe(<<"some/topic">>), + QS = [], + Headers = emqx_mgmt_api_test_util:auth_header_(), + ?assertMatch( + #{<<"data">> := [#{<<"clientid">> := null}]}, + request_json(get, QS, Headers) + ), + ok. + request_json(Method, Query, Headers) when is_list(Query) -> Qs = uri_string:compose_query(Query), {ok, MatchRes} = emqx_mgmt_api_test_util:request_api(Method, path(), Qs, Headers), diff --git a/apps/emqx_plugin_libs/src/emqx_plugin_libs.app.src b/apps/emqx_plugin_libs/src/emqx_plugin_libs.app.src index 605fdb346..dcb330df4 100644 --- a/apps/emqx_plugin_libs/src/emqx_plugin_libs.app.src +++ b/apps/emqx_plugin_libs/src/emqx_plugin_libs.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_plugin_libs, [ {description, "EMQX Plugin utility libs"}, - {vsn, "4.3.7"}, + {vsn, "4.3.8"}, {modules, []}, {applications, [kernel, stdlib]}, {env, []} diff --git a/apps/emqx_plugin_libs/src/emqx_plugin_libs_pool.erl b/apps/emqx_plugin_libs/src/emqx_plugin_libs_pool.erl index 289d39032..9b286f360 100644 --- a/apps/emqx_plugin_libs/src/emqx_plugin_libs_pool.erl +++ b/apps/emqx_plugin_libs/src/emqx_plugin_libs_pool.erl @@ -67,13 +67,14 @@ stop_pool(Name) -> health_check_ecpool_workers(PoolName, CheckFunc) -> health_check_ecpool_workers(PoolName, CheckFunc, ?HEALTH_CHECK_TIMEOUT). -health_check_ecpool_workers(PoolName, CheckFunc, Timeout) when is_function(CheckFunc) -> +health_check_ecpool_workers(PoolName, CheckFunc, Timeout) -> Workers = [Worker || {_WorkerName, Worker} <- ecpool:workers(PoolName)], DoPerWorker = fun(Worker) -> case ecpool_worker:client(Worker) of {ok, Conn} -> - erlang:is_process_alive(Conn) andalso CheckFunc(Conn); + erlang:is_process_alive(Conn) andalso + ecpool_worker:exec(Worker, CheckFunc, Timeout); _ -> false end diff --git a/apps/emqx_prometheus/src/emqx_prometheus.app.src b/apps/emqx_prometheus/src/emqx_prometheus.app.src index 07ae38d75..1e7e59f7a 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.app.src +++ b/apps/emqx_prometheus/src/emqx_prometheus.app.src @@ -2,7 +2,7 @@ {application, emqx_prometheus, [ {description, "Prometheus for EMQX"}, % strict semver, bump manually! - {vsn, "5.0.7"}, + {vsn, "5.0.8"}, {modules, []}, {registered, [emqx_prometheus_sup]}, {applications, [kernel, stdlib, prometheus, emqx, emqx_management]}, diff --git a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl index 6ced0bf42..f8005f06b 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl @@ -90,7 +90,7 @@ fields("prometheus") -> #{ default => enabled, required => true, - hidden => true, + importance => ?IMPORTANCE_HIDDEN, desc => ?DESC(vm_dist_collector) } )}, @@ -100,7 +100,7 @@ fields("prometheus") -> #{ default => enabled, required => true, - hidden => true, + importance => ?IMPORTANCE_HIDDEN, desc => ?DESC(mnesia_collector) } )}, @@ -110,7 +110,7 @@ fields("prometheus") -> #{ default => enabled, required => true, - hidden => true, + importance => ?IMPORTANCE_HIDDEN, desc => ?DESC(vm_statistics_collector) } )}, @@ -120,7 +120,7 @@ fields("prometheus") -> #{ default => enabled, required => true, - hidden => true, + importance => ?IMPORTANCE_HIDDEN, desc => ?DESC(vm_system_info_collector) } )}, @@ -130,7 +130,7 @@ fields("prometheus") -> #{ default => enabled, required => true, - hidden => true, + importance => ?IMPORTANCE_HIDDEN, desc => ?DESC(vm_memory_collector) } )}, @@ -140,7 +140,7 @@ fields("prometheus") -> #{ default => enabled, required => true, - hidden => true, + importance => ?IMPORTANCE_HIDDEN, desc => ?DESC(vm_msacc_collector) } )} diff --git a/apps/emqx_resource/include/emqx_resource.hrl b/apps/emqx_resource/include/emqx_resource.hrl index 41be9e8a0..d799e7d93 100644 --- a/apps/emqx_resource/include/emqx_resource.hrl +++ b/apps/emqx_resource/include/emqx_resource.hrl @@ -41,6 +41,7 @@ callback_mode := callback_mode(), query_mode := query_mode(), config := resource_config(), + error := term(), state := resource_state(), status := resource_status(), metrics => emqx_metrics_worker:metrics() @@ -73,7 +74,7 @@ max_queue_bytes => pos_integer(), query_mode => query_mode(), resume_interval => pos_integer(), - async_inflight_window => pos_integer() + inflight_window => pos_integer() }. -type query_result() :: ok diff --git a/apps/emqx_resource/src/emqx_resource.erl b/apps/emqx_resource/src/emqx_resource.erl index 0ed459c01..0f7e93a9b 100644 --- a/apps/emqx_resource/src/emqx_resource.erl +++ b/apps/emqx_resource/src/emqx_resource.erl @@ -265,7 +265,7 @@ query(ResId, Request, Opts) -> IsBufferSupported = is_buffer_supported(Module), case {IsBufferSupported, QM} of {true, _} -> - %% only Kafka so far + %% only Kafka producer so far Opts1 = Opts#{is_buffer_supported => true}, emqx_resource_buffer_worker:simple_async_query(ResId, Request, Opts1); {false, sync} -> diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 8bfd77e61..0fa4c0bd8 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -88,6 +88,8 @@ -type queue_query() :: ?QUERY(reply_fun(), request(), HasBeenSent :: boolean(), expire_at()). -type request() :: term(). -type request_from() :: undefined | gen_statem:from(). +-type request_timeout() :: infinity | timer:time(). +-type health_check_interval() :: timer:time(). -type state() :: blocked | running. -type inflight_key() :: integer(). -type data() :: #{ @@ -140,7 +142,7 @@ simple_sync_query(Id, Request) -> QueryOpts = simple_query_opts(), emqx_resource_metrics:matched_inc(Id), Ref = make_request_ref(), - Result = call_query(sync, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), + Result = call_query(force_sync, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), _ = handle_query_result(Id, Result, _HasBeenSent = false), Result. @@ -152,7 +154,7 @@ simple_async_query(Id, Request, QueryOpts0) -> QueryOpts = maps:merge(simple_query_opts(), QueryOpts0), emqx_resource_metrics:matched_inc(Id), Ref = make_request_ref(), - Result = call_query(async, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), + Result = call_query(async_if_possible, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), _ = handle_query_result(Id, Result, _HasBeenSent = false), Result. @@ -193,12 +195,14 @@ init({Id, Index, Opts}) -> Queue = replayq:open(QueueOpts), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Queue)), emqx_resource_metrics:inflight_set(Id, Index, 0), - InflightWinSize = maps:get(async_inflight_window, Opts, ?DEFAULT_INFLIGHT), + InflightWinSize = maps:get(inflight_window, Opts, ?DEFAULT_INFLIGHT), InflightTID = inflight_new(InflightWinSize, Id, Index), HealthCheckInterval = maps:get(health_check_interval, Opts, ?HEALTHCHECK_INTERVAL), RequestTimeout = maps:get(request_timeout, Opts, ?DEFAULT_REQUEST_TIMEOUT), BatchTime0 = maps:get(batch_time, Opts, ?DEFAULT_BATCH_TIME), BatchTime = adjust_batch_time(Id, RequestTimeout, BatchTime0), + DefaultResumeInterval = default_resume_interval(RequestTimeout, HealthCheckInterval), + ResumeInterval = maps:get(resume_interval, Opts, DefaultResumeInterval), Data = #{ id => Id, index => Index, @@ -207,7 +211,7 @@ init({Id, Index, Opts}) -> batch_size => BatchSize, batch_time => BatchTime, queue => Queue, - resume_interval => maps:get(resume_interval, Opts, HealthCheckInterval), + resume_interval => ResumeInterval, tref => undefined }, ?tp(buffer_worker_init, #{id => Id, index => Index}), @@ -377,7 +381,7 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> } = Data0, ?tp(buffer_worker_retry_inflight, #{query_or_batch => QueryOrBatch, ref => Ref}), QueryOpts = #{simple_query => false}, - Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), + Result = call_query(force_sync, Id, Index, Ref, QueryOrBatch, QueryOpts), ReplyResult = case QueryOrBatch of ?QUERY(ReplyTo, _, HasBeenSent, _ExpireAt) -> @@ -566,7 +570,7 @@ do_flush( %% unwrap when not batching (i.e., batch size == 1) [?QUERY(ReplyTo, _, HasBeenSent, _ExpireAt) = Request] = Batch, QueryOpts = #{inflight_tid => InflightTID, simple_query => false}, - Result = call_query(configured, Id, Index, Ref, Request, QueryOpts), + Result = call_query(async_if_possible, Id, Index, Ref, Request, QueryOpts), Reply = ?REPLY(ReplyTo, HasBeenSent, Result), case reply_caller(Id, Reply, QueryOpts) of %% Failed; remove the request from the queue, as we cannot pop @@ -651,7 +655,7 @@ do_flush(#{queue := Q1} = Data0, #{ inflight_tid := InflightTID } = Data0, QueryOpts = #{inflight_tid => InflightTID, simple_query => false}, - Result = call_query(configured, Id, Index, Ref, Batch, QueryOpts), + Result = call_query(async_if_possible, Id, Index, Ref, Batch, QueryOpts), case batch_reply_caller(Id, Result, Batch, QueryOpts) of %% Failed; remove the request from the queue, as we cannot pop %% from it again, but we'll retry it using the inflight table. @@ -883,17 +887,13 @@ handle_async_worker_down(Data0, Pid) -> mark_inflight_items_as_retriable(Data, WorkerMRef), {keep_state, Data}. -call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> - ?tp(call_query_enter, #{id => Id, query => Query, query_mode => QM0}), +-spec call_query(force_sync | async_if_possible, _, _, _, _, _) -> _. +call_query(QM, Id, Index, Ref, Query, QueryOpts) -> + ?tp(call_query_enter, #{id => Id, query => Query, query_mode => QM}), case emqx_resource_manager:lookup_cached(Id) of {ok, _Group, #{status := stopped}} -> ?RESOURCE_ERROR(stopped, "resource stopped or disabled"); {ok, _Group, Resource} -> - QM = - case QM0 =:= configured of - true -> maps:get(query_mode, Resource); - false -> QM0 - end, do_call_query(QM, Id, Index, Ref, Query, QueryOpts, Resource); {error, not_found} -> ?RESOURCE_ERROR(not_found, "resource not found") @@ -1511,9 +1511,9 @@ inc_sent_success(Id, _HasBeenSent = true) -> inc_sent_success(Id, _HasBeenSent) -> emqx_resource_metrics:success_inc(Id). -call_mode(sync, _) -> sync; -call_mode(async, always_sync) -> sync; -call_mode(async, async_if_possible) -> async. +call_mode(force_sync, _) -> sync; +call_mode(async_if_possible, always_sync) -> sync; +call_mode(async_if_possible, async_if_possible) -> async. assert_ok_result(ok) -> true; @@ -1679,6 +1679,17 @@ adjust_batch_time(Id, RequestTimeout, BatchTime0) -> end, BatchTime. +%% The request timeout should be greater than the resume interval, as +%% it defines how often the buffer worker tries to unblock. If request +%% timeout is <= resume interval and the buffer worker is ever +%% blocked, than all queued requests will basically fail without being +%% attempted. +-spec default_resume_interval(request_timeout(), health_check_interval()) -> timer:time(). +default_resume_interval(_RequestTimeout = infinity, HealthCheckInterval) -> + max(1, HealthCheckInterval); +default_resume_interval(RequestTimeout, HealthCheckInterval) -> + max(1, min(HealthCheckInterval, RequestTimeout div 3)). + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). adjust_batch_time_test_() -> diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 40f9fe1ab..6a4919b41 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -388,6 +388,7 @@ handle_event(state_timeout, health_check, connecting, Data) -> handle_event(enter, _OldState, connected = State, Data) -> ok = log_state_consistency(State, Data), _ = emqx_alarm:deactivate(Data#data.id), + ?tp(resource_connected_enter, #{}), {keep_state_and_data, health_check_actions(Data)}; handle_event(state_timeout, health_check, connected, Data) -> handle_connected_health_check(Data); @@ -522,7 +523,7 @@ start_resource(Data, From) -> id => Data#data.id, reason => Reason }), - _ = maybe_alarm(disconnected, Data#data.id), + _ = maybe_alarm(disconnected, Data#data.id, Data#data.error), %% Keep track of the error reason why the connection did not work %% so that the Reason can be returned when the verification call is made. UpdatedData = Data#data{status = disconnected, error = Reason}, @@ -597,7 +598,7 @@ with_health_check(Data, Func) -> ResId = Data#data.id, HCRes = emqx_resource:call_health_check(Data#data.manager_id, Data#data.mod, Data#data.state), {Status, NewState, Err} = parse_health_check_result(HCRes, Data), - _ = maybe_alarm(Status, ResId), + _ = maybe_alarm(Status, ResId, Err), ok = maybe_resume_resource_workers(ResId, Status), UpdatedData = Data#data{ state = NewState, status = Status, error = Err @@ -616,15 +617,20 @@ update_state(Data, _DataWas) -> health_check_interval(Opts) -> maps:get(health_check_interval, Opts, ?HEALTHCHECK_INTERVAL). -maybe_alarm(connected, _ResId) -> +maybe_alarm(connected, _ResId, _Error) -> ok; -maybe_alarm(_Status, <>) -> +maybe_alarm(_Status, <>, _Error) -> ok; -maybe_alarm(_Status, ResId) -> +maybe_alarm(_Status, ResId, Error) -> + HrError = + case Error of + undefined -> <<"Unknown reason">>; + _Else -> emqx_misc:readable_error_msg(Error) + end, emqx_alarm:activate( ResId, #{resource_id => ResId, reason => resource_down}, - <<"resource down: ", ResId/binary>> + <<"resource down: ", HrError/binary>> ). maybe_resume_resource_workers(ResId, connected) -> @@ -666,6 +672,7 @@ maybe_reply(Actions, From, Reply) -> data_record_to_external_map(Data) -> #{ id => Data#data.id, + error => Data#data.error, mod => Data#data.mod, callback_mode => Data#data.callback_mode, query_mode => Data#data.query_mode, diff --git a/apps/emqx_resource/src/schema/emqx_resource_schema.erl b/apps/emqx_resource/src/schema/emqx_resource_schema.erl index fdd65bc3c..e89278e8c 100644 --- a/apps/emqx_resource/src/schema/emqx_resource_schema.erl +++ b/apps/emqx_resource/src/schema/emqx_resource_schema.erl @@ -39,10 +39,9 @@ fields("resource_opts_sync_only") -> )} ]; fields("creation_opts_sync_only") -> - Fields0 = fields("creation_opts"), - Fields1 = lists:keydelete(async_inflight_window, 1, Fields0), + Fields = fields("creation_opts"), QueryMod = {query_mode, fun query_mode_sync_only/1}, - lists:keyreplace(query_mode, 1, Fields1, QueryMod); + lists:keyreplace(query_mode, 1, Fields, QueryMod); fields("resource_opts") -> [ {resource_opts, @@ -55,12 +54,13 @@ fields("creation_opts") -> [ {worker_pool_size, fun worker_pool_size/1}, {health_check_interval, fun health_check_interval/1}, + {resume_interval, fun resume_interval/1}, {start_after_created, fun start_after_created/1}, {start_timeout, fun start_timeout/1}, {auto_restart_interval, fun auto_restart_interval/1}, {query_mode, fun query_mode/1}, {request_timeout, fun request_timeout/1}, - {async_inflight_window, fun async_inflight_window/1}, + {inflight_window, fun inflight_window/1}, {enable_batch, fun enable_batch/1}, {batch_size, fun batch_size/1}, {batch_time, fun batch_time/1}, @@ -81,6 +81,12 @@ worker_pool_size(default) -> ?WORKER_POOL_SIZE; worker_pool_size(required) -> false; worker_pool_size(_) -> undefined. +resume_interval(type) -> emqx_schema:duration_ms(); +resume_interval(importance) -> hidden; +resume_interval(desc) -> ?DESC("resume_interval"); +resume_interval(required) -> false; +resume_interval(_) -> undefined. + health_check_interval(type) -> emqx_schema:duration_ms(); health_check_interval(desc) -> ?DESC("health_check_interval"); health_check_interval(default) -> ?HEALTHCHECK_INTERVAL_RAW; @@ -136,11 +142,12 @@ enable_queue(deprecated) -> {since, "v5.0.14"}; enable_queue(desc) -> ?DESC("enable_queue"); enable_queue(_) -> undefined. -async_inflight_window(type) -> pos_integer(); -async_inflight_window(desc) -> ?DESC("async_inflight_window"); -async_inflight_window(default) -> ?DEFAULT_INFLIGHT; -async_inflight_window(required) -> false; -async_inflight_window(_) -> undefined. +inflight_window(type) -> pos_integer(); +inflight_window(aliases) -> [async_inflight_window]; +inflight_window(desc) -> ?DESC("inflight_window"); +inflight_window(default) -> ?DEFAULT_INFLIGHT; +inflight_window(required) -> false; +inflight_window(_) -> undefined. batch_size(type) -> pos_integer(); batch_size(desc) -> ?DESC("batch_size"); diff --git a/apps/emqx_resource/test/emqx_connector_demo.erl b/apps/emqx_resource/test/emqx_connector_demo.erl index a863dbb78..a1393c574 100644 --- a/apps/emqx_resource/test/emqx_connector_demo.erl +++ b/apps/emqx_resource/test/emqx_connector_demo.erl @@ -146,6 +146,12 @@ on_query(_InstId, {sleep_before_reply, For}, #{pid := Pid}) -> {error, timeout} end. +on_query_async(_InstId, block, ReplyFun, #{pid := Pid}) -> + Pid ! {block, ReplyFun}, + {ok, Pid}; +on_query_async(_InstId, resume, ReplyFun, #{pid := Pid}) -> + Pid ! {resume, ReplyFun}, + {ok, Pid}; on_query_async(_InstId, {inc_counter, N}, ReplyFun, #{pid := Pid}) -> Pid ! {inc, N, ReplyFun}, {ok, Pid}; @@ -274,6 +280,10 @@ counter_loop( block -> ct:pal("counter recv: ~p", [block]), State#{status => blocked}; + {block, ReplyFun} -> + ct:pal("counter recv: ~p", [block]), + apply_reply(ReplyFun, ok), + State#{status => blocked}; {block_now, ReplyFun} -> ct:pal("counter recv: ~p", [block_now]), apply_reply( @@ -284,6 +294,11 @@ counter_loop( {messages, Msgs} = erlang:process_info(self(), messages), ct:pal("counter recv: ~p, buffered msgs: ~p", [resume, length(Msgs)]), State#{status => running}; + {resume, ReplyFun} -> + {messages, Msgs} = erlang:process_info(self(), messages), + ct:pal("counter recv: ~p, buffered msgs: ~p", [resume, length(Msgs)]), + apply_reply(ReplyFun, ok), + State#{status => running}; {inc, N, ReplyFun} when Status == running -> %ct:pal("async counter recv: ~p", [{inc, N}]), apply_reply(ReplyFun, ok), diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index e7c252fa9..8638c381f 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -369,7 +369,7 @@ t_query_counter_async_callback(_) -> #{ query_mode => async, batch_size => 1, - async_inflight_window => 1000000 + inflight_window => 1000000 } ), ?assertMatch({ok, 0}, emqx_resource:simple_sync_query(?ID, get_counter)), @@ -450,7 +450,7 @@ t_query_counter_async_inflight(_) -> #{ query_mode => async, batch_size => 1, - async_inflight_window => WindowSize, + inflight_window => WindowSize, worker_pool_size => 1, resume_interval => 300 } @@ -634,7 +634,7 @@ t_query_counter_async_inflight_batch(_) -> query_mode => async, batch_size => BatchSize, batch_time => 100, - async_inflight_window => WindowSize, + inflight_window => WindowSize, worker_pool_size => 1, resume_interval => 300 } @@ -1584,7 +1584,7 @@ t_retry_async_inflight_full(_Config) -> #{name => ?FUNCTION_NAME}, #{ query_mode => async, - async_inflight_window => AsyncInflightWindow, + inflight_window => AsyncInflightWindow, batch_size => 1, worker_pool_size => 1, resume_interval => ResumeInterval @@ -1642,7 +1642,7 @@ t_async_reply_multi_eval(_Config) -> #{name => ?FUNCTION_NAME}, #{ query_mode => async, - async_inflight_window => AsyncInflightWindow, + inflight_window => AsyncInflightWindow, batch_size => 3, batch_time => 10, worker_pool_size => 1, @@ -2561,6 +2561,84 @@ do_t_recursive_flush() -> ), ok. +t_call_mode_uncoupled_from_query_mode(_Config) -> + DefaultOpts = #{ + batch_size => 1, + batch_time => 5, + worker_pool_size => 1 + }, + ?check_trace( + begin + %% We check that we can call the buffer workers with async + %% calls, even if the underlying connector itself only + %% supports sync calls. + emqx_connector_demo:set_callback_mode(always_sync), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + DefaultOpts#{query_mode => async} + ), + ?tp_span( + async_query_sync_driver, + #{}, + ?assertMatch( + {ok, {ok, _}}, + ?wait_async_action( + emqx_resource:query(?ID, {inc_counter, 1}), + #{?snk_kind := buffer_worker_flush_ack}, + 500 + ) + ) + ), + ?assertEqual(ok, emqx_resource:remove_local(?ID)), + + %% And we check the converse: a connector that allows async + %% calls can be called synchronously, but the underlying + %% call should be async. + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + DefaultOpts#{query_mode => sync} + ), + ?tp_span( + sync_query_async_driver, + #{}, + ?assertEqual(ok, emqx_resource:query(?ID, {inc_counter, 2})) + ), + ?assertEqual(ok, emqx_resource:remove_local(?ID)), + ?tp(sync_query_async_driver, #{}), + ok + end, + fun(Trace0) -> + Trace1 = trace_between_span(Trace0, async_query_sync_driver), + ct:pal("async query calling sync driver\n ~p", [Trace1]), + ?assert( + ?strict_causality( + #{?snk_kind := async_query, request := {inc_counter, 1}}, + #{?snk_kind := call_query, call_mode := sync}, + Trace1 + ) + ), + + Trace2 = trace_between_span(Trace0, sync_query_async_driver), + ct:pal("sync query calling async driver\n ~p", [Trace2]), + ?assert( + ?strict_causality( + #{?snk_kind := sync_query, request := {inc_counter, 2}}, + #{?snk_kind := call_query_async}, + Trace2 + ) + ), + + ok + end + ). + %%------------------------------------------------------------------------------ %% Helpers %%------------------------------------------------------------------------------ @@ -2742,3 +2820,8 @@ assert_async_retry_fail_then_succeed_inflight(Trace) -> ) ), ok. + +trace_between_span(Trace0, Marker) -> + {Trace1, [_ | _]} = ?split_trace_at(#{?snk_kind := Marker, ?snk_span := {complete, _}}, Trace0), + {[_ | _], [_ | Trace2]} = ?split_trace_at(#{?snk_kind := Marker, ?snk_span := start}, Trace1), + Trace2. diff --git a/apps/emqx_rule_engine/README.md b/apps/emqx_rule_engine/README.md index 2485ff534..2c2e43db3 100644 --- a/apps/emqx_rule_engine/README.md +++ b/apps/emqx_rule_engine/README.md @@ -1,23 +1,46 @@ -# emqx-rule-engine +# Emqx Rule Engine -IoT Rule Engine +The rule engine's goal is to provide a simple and flexible way to transform and +reroute the messages coming to the EMQX broker. For example, one message +containing measurements from multiple sensors of different types can be +transformed into multiple messages. + + +## Concepts + +A rule is quite simple. A rule describes which messages it affects by +specifying a topic filter and a set of conditions that need to be met. If a +message matches the topic filter and all the conditions are met, the rule is +triggered. The rule can then transform the message and route it to a different +topic, or send it to another service (defined by an EMQX bridge). The rule +engine's message data transformation is designed to work well with structured data +such as JSON, avro, and protobuf. + + +A rule consists of the three parts **MATCH**, **TRANSFORM** and **ACTIONS** that are +described below: + +* **MATCH** - The rule's trigger condition. The rule is triggered when a message + arrives that matches the topic filter and all the specified conditions are met. +* **TRANSFORM** - The rule's data transformation. The rule can select data from the + incoming message and transform it into a new message. +* **ACTIONS** - The rule's action(s). The rule can have one or more actions. The + actions are executed when the rule is triggered. The actions can be to route + the message to a different topic, or send it to another service (defined by + an EMQX bridge). -## Concept -``` -iot rule "Rule Name" - when - match TopicFilters and Conditions - select - para1 = val1 - para2 = val2 - then - take action(#{para2 => val1, #para2 => val2}) -``` ## Architecture +The following diagram shows how the rule engine is integrated with the EMQX +message broker. Incoming messages are checked against the rules, and if a rule +matches, it is triggered with the message as input. The rule can then transform +or split the message and/or route it to a different topic, or send it to another +service (defined by an EMQX bridge). + + ``` |-----------------| Pub ---->| Message Routing |----> Sub @@ -28,11 +51,33 @@ iot rule "Rule Name" | Rule Engine | |-----------------| | | - Backends Services Bridges + Services Bridges (defined by EMQX bridges) ``` -## SQL for Rule query statement +## Domain Specific Language for Rules + +The **MATCH** and **TRANSFORM** parts of the rule are specified using a domain +specific language that looks similar to SQL. The following is an example of a +rule engine statement. The `from "topic/a"` part specifies the topic filter +(only messages to the topic `topic/a` will be considered). The `where t > 50` +part specifies the condition that needs to be met for the rule to be triggered. +The `select id, time, temperature as t` part specifies the data transformation +(the selected fields will remain in the transformed message payload). The `as +t` part specifies that the `temperature` field name is changed to `t` in the +output message. The name `t` can also be used in the where part of the rule as +an alias for `t`. + ``` -select id, time, temperature as t from "topic/a" where t > 50; +select id, time, temperature as t from "topic/a" where t > 50 ``` + + This just scratches the surface of what is possible with the rule engine. The + full documentation is available at [EMQX Rule + Engine](https://www.emqx.io/docs/en/v5.0/data-integration/rules.html). For + example, there are many built-in functions that can be used in the rule engine + language to help in doing transformations and matching. One of the [built-in + functions allows you to run JQ + queries](https://www.emqx.io/docs/en/v5.0/data-integration/rule-sql-jq.html) + which allows you to do complex transformations of the message. + diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl index 30de3e8e8..106693a0a 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl @@ -180,7 +180,7 @@ schema("/rules") -> ref(emqx_dashboard_swagger, page), ref(emqx_dashboard_swagger, limit) ], - summary => <<"List Rules">>, + summary => <<"List rules">>, responses => #{ 200 => [ @@ -193,7 +193,7 @@ schema("/rules") -> post => #{ tags => [<<"rules">>], description => ?DESC("api2"), - summary => <<"Create a Rule">>, + summary => <<"Create a rule">>, 'requestBody' => rule_creation_schema(), responses => #{ 400 => error_schema('BAD_REQUEST', "Invalid Parameters"), @@ -207,7 +207,7 @@ schema("/rule_events") -> get => #{ tags => [<<"rules">>], description => ?DESC("api3"), - summary => <<"List Events">>, + summary => <<"List rule events">>, responses => #{ 200 => mk(ref(emqx_rule_api_schema, "rule_events"), #{}) } @@ -219,7 +219,7 @@ schema("/rules/:id") -> get => #{ tags => [<<"rules">>], description => ?DESC("api4"), - summary => <<"Get a Rule">>, + summary => <<"Get rule">>, parameters => param_path_id(), responses => #{ 404 => error_schema('NOT_FOUND', "Rule not found"), @@ -229,7 +229,7 @@ schema("/rules/:id") -> put => #{ tags => [<<"rules">>], description => ?DESC("api5"), - summary => <<"Update a Rule">>, + summary => <<"Update rule">>, parameters => param_path_id(), 'requestBody' => rule_creation_schema(), responses => #{ @@ -240,7 +240,7 @@ schema("/rules/:id") -> delete => #{ tags => [<<"rules">>], description => ?DESC("api6"), - summary => <<"Delete a Rule">>, + summary => <<"Delete rule">>, parameters => param_path_id(), responses => #{ 204 => <<"Delete rule successfully">> @@ -253,7 +253,7 @@ schema("/rules/:id/metrics") -> get => #{ tags => [<<"rules">>], description => ?DESC("api4_1"), - summary => <<"Get a Rule's Metrics">>, + summary => <<"Get rule metrics">>, parameters => param_path_id(), responses => #{ 404 => error_schema('NOT_FOUND', "Rule not found"), @@ -267,7 +267,7 @@ schema("/rules/:id/metrics/reset") -> put => #{ tags => [<<"rules">>], description => ?DESC("api7"), - summary => <<"Reset a Rule Metrics">>, + summary => <<"Reset rule metrics">>, parameters => param_path_id(), responses => #{ 404 => error_schema('NOT_FOUND', "Rule not found"), @@ -281,7 +281,7 @@ schema("/rule_test") -> post => #{ tags => [<<"rules">>], description => ?DESC("api8"), - summary => <<"Test a Rule">>, + summary => <<"Test a rule">>, 'requestBody' => rule_test_schema(), responses => #{ 400 => error_schema('BAD_REQUEST', "Invalid Parameters"), diff --git a/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl index 5d78f5e4a..209332fe7 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl @@ -28,7 +28,7 @@ init_per_suite(Config) -> application:load(emqx_conf), - ConfigConf = <<"rule_engine {jq_function_default_timeout {}}">>, + ConfigConf = <<"rule_engine {jq_function_default_timeout=10s}">>, ok = emqx_common_test_helpers:load_config(emqx_rule_engine_schema, ConfigConf), Config. @@ -691,20 +691,10 @@ t_jq(_) -> ConfigRootKey, jq_function_default_timeout ]), - case DefaultTimeOut =< 15000 of - true -> - got_timeout = - try - apply_func(jq, [TOProgram, <<"-2">>]) - catch - throw:{jq_exception, {timeout, _}} -> - %% Got timeout as expected - got_timeout - end; - false -> - %% Skip test as we don't want it to take to long time to run - ok - end. + ?assertThrow( + {jq_exception, {timeout, _}}, + apply_func(jq, [TOProgram, <<"-2">>]) + ). ascii_string() -> list(range(0, 127)). diff --git a/bin/emqx b/bin/emqx index 741aa3718..fc0124b96 100755 --- a/bin/emqx +++ b/bin/emqx @@ -159,10 +159,10 @@ usage() { echo "Print EMQX installation root dir" ;; eval) - echo "Evaluate an Erlang or Elixir expression in the EMQX node" + echo "Evaluate an Erlang expression in the EMQX node." ;; - eval-erl) - echo "Evaluate an Erlang expression in the EMQX node, even on Elixir node" + eval-ex) + echo "Evaluate an Elixir expression in the EMQX node. Only applies to Elixir node" ;; versions) echo "List installed EMQX release versions and their status" @@ -228,7 +228,7 @@ usage() { echo " Install Info: ertspath | root_dir" echo " Runtime Status: pid | ping" echo " Validate Config: check_config" - echo " Advanced: console_clean | escript | rpc | rpcterms | eval | eval-erl" + echo " Advanced: console_clean | escript | rpc | rpcterms | eval | eval-ex" echo '' echo "Execute '$REL_NAME COMMAND help' for more information" ;; @@ -372,12 +372,6 @@ maybe_use_portable_dynlibs() { fi } -# Warn the user if ulimit -n is less than 1024 -ULIMIT_F=$(ulimit -n) -if [ "$ULIMIT_F" -lt 1024 ]; then - logwarn "ulimit -n is ${ULIMIT_F}; 1024 is the recommended minimum." -fi - SED_REPLACE="sed -i " case $(sed --help 2>&1) in *GNU*) SED_REPLACE="sed -i ";; @@ -763,7 +757,7 @@ generate_config() { local node_name="$2" ## Delete the *.siz files first or it can't start after ## changing the config 'log.rotation.size' - rm -rf "${RUNNER_LOG_DIR}"/*.siz + rm -f "${RUNNER_LOG_DIR}"/*.siz ## timestamp for each generation local NOW_TIME @@ -909,6 +903,14 @@ maybe_log_to_console() { fi } +# Warn the user if ulimit -n is less than 1024 +maybe_warn_ulimit() { + ULIMIT_F=$(ulimit -n) + if [ "$ULIMIT_F" -lt 1024 ]; then + logwarn "ulimit -n is ${ULIMIT_F}; 1024 is the recommended minimum." + fi +} + ## Possible ways to configure emqx node name: ## 1. configure node.name in emqx.conf ## 2. override with environment variable EMQX_NODE__NAME @@ -1030,6 +1032,7 @@ cd "$RUNNER_ROOT_DIR" case "${COMMAND}" in start) + maybe_warn_ulimit maybe_warn_default_cookie # this flag passes down to console mode @@ -1181,6 +1184,7 @@ case "${COMMAND}" in tr_log_to_env else maybe_log_to_console + maybe_warn_ulimit maybe_warn_default_cookie fi @@ -1258,6 +1262,12 @@ case "${COMMAND}" in eval) assert_node_alive + shift + relx_nodetool "eval" "$@" + ;; + eval-ex) + assert_node_alive + shift if [ "$IS_ELIXIR" = "yes" ] then @@ -1271,16 +1281,11 @@ case "${COMMAND}" in --erl "-start_epmd false -epmd_module ekka_epmd" \ --rpc-eval "$NAME" "$@" else - relx_nodetool "eval" "$@" + echo "EMQX node is not an Elixir node" + usage "$COMMAND" + exit 1 fi ;; - eval-erl) - assert_node_alive - - shift - relx_nodetool "eval" "$@" - ;; - check_config) check_config ;; diff --git a/bin/node_dump b/bin/node_dump index 09baf04fd..1c4df08b5 100755 --- a/bin/node_dump +++ b/bin/node_dump @@ -49,7 +49,7 @@ done # Collect system info: { collect "$RUNNER_BIN_DIR"/emqx_ctl broker - collect "$RUNNER_BIN_DIR"/emqx eval-erl "'emqx_node_dump:sys_info()'" + collect "$RUNNER_BIN_DIR"/emqx eval "'emqx_node_dump:sys_info()'" collect uname -a collect uptime @@ -64,7 +64,7 @@ done # Collect information about the configuration: { - collect "$RUNNER_BIN_DIR"/emqx eval-erl "'emqx_node_dump:app_env_dump()'" + collect "$RUNNER_BIN_DIR"/emqx eval "'emqx_node_dump:app_env_dump()'" } > "${CONF_DUMP}" # Collect license info: diff --git a/build b/build index 76298f1ab..3c558c19a 100755 --- a/build +++ b/build @@ -147,7 +147,7 @@ make_rel() { make_elixir_rel() { ./scripts/pre-compile.sh "$PROFILE" - export_release_vars "$PROFILE" + export_elixir_release_vars "$PROFILE" # for some reason, this has to be run outside "do"... mix local.rebar --if-missing --force # shellcheck disable=SC1010 @@ -362,7 +362,7 @@ function join { # used to control the Elixir Mix Release output # see docstring in `mix.exs` -export_release_vars() { +export_elixir_release_vars() { local profile="$1" case "$profile" in emqx|emqx-enterprise) @@ -376,27 +376,6 @@ export_release_vars() { exit 1 esac export MIX_ENV="$profile" - - local erl_opts=() - - case "$(is_enterprise "$profile")" in - 'yes') - erl_opts+=( "{d, 'EMQX_RELEASE_EDITION', ee}" ) - ;; - 'no') - erl_opts+=( "{d, 'EMQX_RELEASE_EDITION', ce}" ) - ;; - esac - - # At this time, Mix provides no easy way to pass `erl_opts' to - # dependencies. The workaround is to set this variable before - # compiling the project, so that `emqx_release.erl' picks up - # `emqx_vsn' as if it was compiled by rebar3. - erl_opts+=( "{compile_info,[{emqx_vsn,\"${PKG_VSN}\"}]}" ) - erl_opts+=( "{d,snk_kind,msg}" ) - - ERL_COMPILER_OPTIONS="[$(join , "${erl_opts[@]}")]" - export ERL_COMPILER_OPTIONS } log "building artifact=$ARTIFACT for profile=$PROFILE" diff --git a/changes/ce/feat-10019.en.md b/changes/ce/feat-10019.en.md deleted file mode 100644 index b6cc0381c..000000000 --- a/changes/ce/feat-10019.en.md +++ /dev/null @@ -1 +0,0 @@ -Add low level tuning settings for QUIC listeners. diff --git a/changes/ce/feat-10019.zh.md b/changes/ce/feat-10019.zh.md deleted file mode 100644 index b0eb2a673..000000000 --- a/changes/ce/feat-10019.zh.md +++ /dev/null @@ -1 +0,0 @@ -为 QUIC 监听器添加更多底层调优选项。 diff --git a/changes/ce/feat-10022.en.md b/changes/ce/feat-10022.en.md deleted file mode 100644 index 61d027aa2..000000000 --- a/changes/ce/feat-10022.en.md +++ /dev/null @@ -1 +0,0 @@ -Start releasing Rocky Linux 9 (compatible with Enterprise Linux 9) and MacOS 12 packages diff --git a/changes/ce/feat-10059.en.md b/changes/ce/feat-10059.en.md deleted file mode 100644 index 2c4de015c..000000000 --- a/changes/ce/feat-10059.en.md +++ /dev/null @@ -1 +0,0 @@ -Errors returned by rule engine API are formatted in a more human readable way rather than dumping the raw error including the stacktrace. diff --git a/changes/ce/feat-10059.zh.md b/changes/ce/feat-10059.zh.md deleted file mode 100644 index 99f8fe8ee..000000000 --- a/changes/ce/feat-10059.zh.md +++ /dev/null @@ -1 +0,0 @@ -规则引擎 API 返回用户可读的错误信息而不是原始的栈追踪信息。 diff --git a/changes/ce/feat-10077.en.md b/changes/ce/feat-10077.en.md new file mode 100644 index 000000000..923e21fa1 --- /dev/null +++ b/changes/ce/feat-10077.en.md @@ -0,0 +1,2 @@ +Add support for QUIC TLS password protected certificate file. + diff --git a/changes/ce/feat-10077.zh.md b/changes/ce/feat-10077.zh.md new file mode 100644 index 000000000..e9c7b5625 --- /dev/null +++ b/changes/ce/feat-10077.zh.md @@ -0,0 +1 @@ +增加对 QUIC TLS 密码保护证书文件的支持。 diff --git a/changes/ce/feat-10128.en.md b/changes/ce/feat-10128.en.md new file mode 100644 index 000000000..ab3e5ba3e --- /dev/null +++ b/changes/ce/feat-10128.en.md @@ -0,0 +1 @@ +Add support for OCSP stapling for SSL MQTT listeners. diff --git a/changes/ce/feat-10164.en.md b/changes/ce/feat-10164.en.md new file mode 100644 index 000000000..9acea755f --- /dev/null +++ b/changes/ce/feat-10164.en.md @@ -0,0 +1 @@ +Add CRL check support for TLS MQTT listeners. diff --git a/changes/ce/feat-10206.en.md b/changes/ce/feat-10206.en.md new file mode 100644 index 000000000..014ea71f2 --- /dev/null +++ b/changes/ce/feat-10206.en.md @@ -0,0 +1,7 @@ +Decouple the query mode from the underlying call mode for buffer +workers. + +Prior to this change, setting the query mode of a resource +such as a bridge to `sync` would force the buffer to call the +underlying connector in a synchronous way, even if it supports async +calls. diff --git a/changes/ce/feat-10207.en.md b/changes/ce/feat-10207.en.md new file mode 100644 index 000000000..99ca17944 --- /dev/null +++ b/changes/ce/feat-10207.en.md @@ -0,0 +1 @@ +Use 'label' from i18n file as 'summary' in OpenAPI spec. diff --git a/changes/ce/feat-10210.en.md b/changes/ce/feat-10210.en.md new file mode 100644 index 000000000..2894ee44e --- /dev/null +++ b/changes/ce/feat-10210.en.md @@ -0,0 +1,4 @@ +Unregister Mnesia post commit hook when Mria is being stopped. +This fixes hook failures occasionally occurring on stopping/restarting Mria. + +[Mria PR](https://github.com/emqx/mria/pull/133) diff --git a/changes/ce/feat-10224.en.md b/changes/ce/feat-10224.en.md new file mode 100644 index 000000000..7ef3e7f99 --- /dev/null +++ b/changes/ce/feat-10224.en.md @@ -0,0 +1 @@ +Add the option to customize `clusterIP` in Helm chart, so that a user may set it to a fixed IP. diff --git a/changes/ce/feat-10263.en.md b/changes/ce/feat-10263.en.md new file mode 100644 index 000000000..e069fc17f --- /dev/null +++ b/changes/ce/feat-10263.en.md @@ -0,0 +1 @@ +Add command 'eval-ex' for Elixir expression evaluation. diff --git a/changes/ce/feat-9213.en.md b/changes/ce/feat-9213.en.md deleted file mode 100644 index 3266ed836..000000000 --- a/changes/ce/feat-9213.en.md +++ /dev/null @@ -1 +0,0 @@ -Add pod disruption budget to helm chart diff --git a/changes/ce/feat-9213.zh.md b/changes/ce/feat-9213.zh.md deleted file mode 100644 index 66cb2693e..000000000 --- a/changes/ce/feat-9213.zh.md +++ /dev/null @@ -1 +0,0 @@ -在 Helm chart 中添加干扰预算 (disruption budget)。 diff --git a/changes/ce/feat-9893.en.md b/changes/ce/feat-9893.en.md deleted file mode 100644 index 343c3794f..000000000 --- a/changes/ce/feat-9893.en.md +++ /dev/null @@ -1,2 +0,0 @@ -When connecting with the flag `clean_start=false`, EMQX will filter out messages that published by banned clients. -Previously, the messages sent by banned clients may still be delivered to subscribers in this scenario. diff --git a/changes/ce/feat-9893.zh.md b/changes/ce/feat-9893.zh.md deleted file mode 100644 index 426439c3e..000000000 --- a/changes/ce/feat-9893.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -当使用 `clean_start=false` 标志连接时,EMQX 将会从消息队列中过滤出被封禁客户端发出的消息,使它们不能被下发给订阅者。 -此前被封禁客户端发出的消息仍可能在这一场景下被下发给订阅者。 diff --git a/changes/ce/feat-9949.en.md b/changes/ce/feat-9949.en.md deleted file mode 100644 index 3ed9c30b2..000000000 --- a/changes/ce/feat-9949.en.md +++ /dev/null @@ -1,2 +0,0 @@ -QUIC transport Multistreams support and QUIC TLS cacert support. - diff --git a/changes/ce/feat-9949.zh.md b/changes/ce/feat-9949.zh.md deleted file mode 100644 index 6efabac3f..000000000 --- a/changes/ce/feat-9949.zh.md +++ /dev/null @@ -1 +0,0 @@ -QUIC 传输多流支持和 QUIC TLS cacert 支持。 diff --git a/changes/ce/feat-9986.en.md b/changes/ce/feat-9986.en.md deleted file mode 100644 index ee7a6be71..000000000 --- a/changes/ce/feat-9986.en.md +++ /dev/null @@ -1 +0,0 @@ -For helm charts, add MQTT ingress bridge; and removed stale `mgmt` references. diff --git a/changes/ce/feat-9986.zh.md b/changes/ce/feat-9986.zh.md deleted file mode 100644 index a7f418587..000000000 --- a/changes/ce/feat-9986.zh.md +++ /dev/null @@ -1 +0,0 @@ -在 helm chart 中新增了 MQTT 桥接 ingress 的配置参数;并删除了旧版本遗留的 `mgmt` 配置。 diff --git a/changes/ce/fix-10009.en.md b/changes/ce/fix-10009.en.md deleted file mode 100644 index 37f33a958..000000000 --- a/changes/ce/fix-10009.en.md +++ /dev/null @@ -1 +0,0 @@ -Validate `bytes` param to `GET /trace/:name/log` to not exceed signed 32bit integer. diff --git a/changes/ce/fix-10009.zh.md b/changes/ce/fix-10009.zh.md deleted file mode 100644 index bb55ea5b9..000000000 --- a/changes/ce/fix-10009.zh.md +++ /dev/null @@ -1 +0,0 @@ -验证 `GET /trace/:name/log` 的 `bytes` 参数,使其不超过有符号的32位整数。 diff --git a/changes/ce/fix-10013.en.md b/changes/ce/fix-10013.en.md deleted file mode 100644 index ed7fa21eb..000000000 --- a/changes/ce/fix-10013.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix return type structure for error case in API schema for `/gateways/:name/clients`. diff --git a/changes/ce/fix-10013.zh.md b/changes/ce/fix-10013.zh.md deleted file mode 100644 index 171b79538..000000000 --- a/changes/ce/fix-10013.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 API `/gateways/:name/clients` 返回值的类型结构错误。 diff --git a/changes/ce/fix-10014.en.md b/changes/ce/fix-10014.en.md deleted file mode 100644 index d52452bf9..000000000 --- a/changes/ce/fix-10014.en.md +++ /dev/null @@ -1 +0,0 @@ -In dashboard API for `/monitor(_current)/nodes/:node` return `404` instead of `400` if node does not exist. diff --git a/changes/ce/fix-10014.zh.md b/changes/ce/fix-10014.zh.md deleted file mode 100644 index 5e6a1660f..000000000 --- a/changes/ce/fix-10014.zh.md +++ /dev/null @@ -1 +0,0 @@ -如果 API 查询的节点不存在,将会返回 404 而不再是 400。 diff --git a/changes/ce/fix-10015.en.md b/changes/ce/fix-10015.en.md deleted file mode 100644 index 5727a52cd..000000000 --- a/changes/ce/fix-10015.en.md +++ /dev/null @@ -1,7 +0,0 @@ -To prevent errors caused by an incorrect EMQX node cookie provided from an environment variable, -we have implemented a fail-fast mechanism. -Previously, when an incorrect cookie was provided, the command would still attempt to ping the node, -leading to the error message 'Node xxx not responding to pings'. -With the new implementation, if a mismatched cookie is detected, -a message will be logged to indicate that the cookie is incorrect, -and the command will terminate with an error code of 1 without trying to ping the node. diff --git a/changes/ce/fix-10015.zh.md b/changes/ce/fix-10015.zh.md deleted file mode 100644 index 0f58fa99c..000000000 --- a/changes/ce/fix-10015.zh.md +++ /dev/null @@ -1,4 +0,0 @@ -在 cookie 给错时,快速失败。 -在此修复前,即使 cookie 配置错误,emqx 命令仍然会尝试去 ping EMQX 节点, -并得到一个 "Node xxx not responding to pings" 的错误。 -修复后,如果发现 cookie 不一致,立即打印不一致的错误信息并退出。 diff --git a/changes/ce/fix-10020.en.md b/changes/ce/fix-10020.en.md deleted file mode 100644 index 73615804b..000000000 --- a/changes/ce/fix-10020.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix bridge metrics when running in async mode with batching enabled (`batch_size` > 1). diff --git a/changes/ce/fix-10020.zh.md b/changes/ce/fix-10020.zh.md deleted file mode 100644 index 2fce853e3..000000000 --- a/changes/ce/fix-10020.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复使用异步和批量配置的桥接计数不准确的问题。 diff --git a/changes/ce/fix-10021.en.md b/changes/ce/fix-10021.en.md deleted file mode 100644 index 28302da70..000000000 --- a/changes/ce/fix-10021.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix error message when the target node of `emqx_ctl cluster join` command is not running. diff --git a/changes/ce/fix-10021.zh.md b/changes/ce/fix-10021.zh.md deleted file mode 100644 index 6df64b76d..000000000 --- a/changes/ce/fix-10021.zh.md +++ /dev/null @@ -1 +0,0 @@ -修正当`emqx_ctl cluster join`命令的目标节点未运行时的错误信息。 diff --git a/changes/ce/fix-10027.en.md b/changes/ce/fix-10027.en.md deleted file mode 100644 index 531da1c50..000000000 --- a/changes/ce/fix-10027.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Allow setting node name from `EMQX_NODE__NAME` when running in docker. -Prior to this fix, only `EMQX_NODE_NAME` is allowed. diff --git a/changes/ce/fix-10027.zh.md b/changes/ce/fix-10027.zh.md deleted file mode 100644 index ee7055d6c..000000000 --- a/changes/ce/fix-10027.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -在 docker 中启动时,允许使用 `EMQX_NODE__NAME` 环境变量来配置节点名。 -在此修复前,只能使 `EMQX_NODE_NAME`。 diff --git a/changes/ce/fix-10032.en.md b/changes/ce/fix-10032.en.md deleted file mode 100644 index bd730c96c..000000000 --- a/changes/ce/fix-10032.en.md +++ /dev/null @@ -1 +0,0 @@ -When resources on some nodes in the cluster are still in the 'initializing/connecting' state, the `bridges/` API will crash due to missing Metrics information for those resources. This fix will ignore resources that do not have Metrics information. diff --git a/changes/ce/fix-10032.zh.md b/changes/ce/fix-10032.zh.md deleted file mode 100644 index fc1fb38b6..000000000 --- a/changes/ce/fix-10032.zh.md +++ /dev/null @@ -1 +0,0 @@ -当集群中某些节点上的资源仍处于 '初始化/连接中' 状态时,`bridges/` API 将由于缺少这些资源的 Metrics 信息而崩溃。此修复后将忽略没有 Metrics 信息的资源。 diff --git a/changes/ce/fix-10037.en.md b/changes/ce/fix-10037.en.md deleted file mode 100644 index 73c92d69d..000000000 --- a/changes/ce/fix-10037.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Fix Swagger API doc rendering crash. -In version 5.0.18, a bug was introduced that resulted in duplicated field names in the configuration schema. This, in turn, caused the Swagger schema generated to become invalid. diff --git a/changes/ce/fix-10037.zh.md b/changes/ce/fix-10037.zh.md deleted file mode 100644 index 5bd447c1f..000000000 --- a/changes/ce/fix-10037.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -修复 Swagger API 文档渲染崩溃。 -在版本 5.0.18 中,引入了一个错误,导致配置 schema 中出现了重复的配置名称,进而导致生成了无效的 Swagger spec。 diff --git a/changes/ce/fix-10041.en.md b/changes/ce/fix-10041.en.md deleted file mode 100644 index c1aff24c2..000000000 --- a/changes/ce/fix-10041.en.md +++ /dev/null @@ -1,2 +0,0 @@ -For influxdb bridge, added integer value placeholder annotation hint to `write_syntax` documentation. -Also supported setting a constant value for the `timestamp` field. diff --git a/changes/ce/fix-10041.zh.md b/changes/ce/fix-10041.zh.md deleted file mode 100644 index d197ea81f..000000000 --- a/changes/ce/fix-10041.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -为 influxdb 桥接的配置项 `write_syntax` 描述文档增加了类型标识符的提醒。 -另外在配置中支持 `timestamp` 使用一个常量。 diff --git a/changes/ce/fix-10042.en.md b/changes/ce/fix-10042.en.md deleted file mode 100644 index af9213c06..000000000 --- a/changes/ce/fix-10042.en.md +++ /dev/null @@ -1,5 +0,0 @@ -Improve behavior of the `replicant` nodes when the `core` cluster becomes partitioned (for example when a core node leaves the cluster). -Previously, the replicant nodes were unable to rebalance connections to the core nodes, until the core cluster became whole again. -This was indicated by the error messages: `[error] line: 182, mfa: mria_lb:list_core_nodes/1, msg: mria_lb_core_discovery divergent cluster`. - -[Mria PR](https://github.com/emqx/mria/pull/123/files) diff --git a/changes/ce/fix-10042.zh.md b/changes/ce/fix-10042.zh.md deleted file mode 100644 index 80db204e2..000000000 --- a/changes/ce/fix-10042.zh.md +++ /dev/null @@ -1,6 +0,0 @@ -改进 `core` 集群被分割时 `replicant`节点的行为。 -修复前,如果 `core` 集群分裂成两个小集群(例如一个节点离开集群)时,`replicant` 节点无法重新平衡与核心节点的连接,直到核心集群再次变得完整。 -这种个问题会导致 replicant 节点出现如下日志: -`[error] line: 182, mfa: mria_lb:list_core_nodes/1, msg: mria_lb_core_discovery divergent cluster`。 - -[Mria PR](https://github.com/emqx/mria/pull/123/files) diff --git a/changes/ce/fix-10043.en.md b/changes/ce/fix-10043.en.md deleted file mode 100644 index 4fd46cb4e..000000000 --- a/changes/ce/fix-10043.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Fixed two bugs introduced in v5.0.18. -* The environment varialbe `SSL_DIST_OPTFILE` was not set correctly for non-boot commands. -* When cookie is overridden from environment variable, EMQX node is unable to start. diff --git a/changes/ce/fix-10043.zh.md b/changes/ce/fix-10043.zh.md deleted file mode 100644 index 6b150f6fb..000000000 --- a/changes/ce/fix-10043.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -修复 v5.0.18 引入的 2 个bug。 -* 环境变量 `SSL_DIST_OPTFILE` 的值设置错误导致节点无法为 Erlang distribution 启用 SSL。 -* 当节点的 cookie 从环境变量重载 (而不是设置在配置文件中时),节点无法启动的问题。 diff --git a/changes/ce/fix-10044.en.md b/changes/ce/fix-10044.en.md deleted file mode 100644 index 00668c5cb..000000000 --- a/changes/ce/fix-10044.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix node information formatter for stopped nodes in the cluster. The bug was introduced by v5.0.18. diff --git a/changes/ce/fix-10044.zh.md b/changes/ce/fix-10044.zh.md deleted file mode 100644 index 72759d707..000000000 --- a/changes/ce/fix-10044.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复集群中已停止节点的信息序列化问题,该错误由 v5.0.18 引入。 diff --git a/changes/ce/fix-10050.en.md b/changes/ce/fix-10050.en.md deleted file mode 100644 index c225c380d..000000000 --- a/changes/ce/fix-10050.en.md +++ /dev/null @@ -1 +0,0 @@ -Ensure Bridge API returns `404` status code consistently for resources that don't exist. diff --git a/changes/ce/fix-10050.zh.md b/changes/ce/fix-10050.zh.md deleted file mode 100644 index d7faf9434..000000000 --- a/changes/ce/fix-10050.zh.md +++ /dev/null @@ -1 +0,0 @@ -确保 Bridge API 对不存在的资源一致返回 `404` 状态代码。 diff --git a/changes/ce/fix-10052.en.md b/changes/ce/fix-10052.en.md deleted file mode 100644 index f83c4d40c..000000000 --- a/changes/ce/fix-10052.en.md +++ /dev/null @@ -1,12 +0,0 @@ -Improve daemon mode startup failure logs. - -Before this change, it was difficult for users to understand the reason for EMQX 'start' command failed to boot the node. -The only information they received was that the node did not start within the expected time frame, -and they were instructed to boot the node with 'console' command in the hope of obtaining some logs. -However, the node might actually be running, which could cause 'console' mode to fail for a different reason. - -With this new change, when daemon mode fails to boot, a diagnosis is issued. Here are the possible scenarios: - -* If the node cannot be found from `ps -ef`, the user is instructed to find information in log files `erlang.log.*`. -* If the node is found to be running but not responding to pings, the user is advised to check if the host name is resolvable and reachable. -* If the node is responding to pings, but the EMQX app is not running, it is likely a bug. In this case, the user is advised to report a Github issue. diff --git a/changes/ce/fix-10052.zh.md b/changes/ce/fix-10052.zh.md deleted file mode 100644 index 1c2eff342..000000000 --- a/changes/ce/fix-10052.zh.md +++ /dev/null @@ -1,11 +0,0 @@ -优化 EMQX daemon 模式启动启动失败的日志。 - -在进行此更改之前,当 EMQX 用 `start` 命令启动失败时,用户很难理解出错的原因。 -所知道的仅仅是节点未能在预期时间内启动,然后被指示以 `console` 式引导节点以获取一些日志。 -然而,节点实际上可能正在运行,这可能会导致 `console` 模式因不同的原因而失败。 - -此次修复后,启动脚本会发出诊断: - -* 如果无法从 `ps -ef` 中找到节点,则指示用户在 `erlang.log.*` 中查找信息。 -* 如果发现节点正在运行但不响应 ping,则建议用户检查节点主机名是否有效并可达。 -* 如果节点响应 ping 但 EMQX 应用程序未运行,则很可能是一个错误。在这种情况下,建议用户报告一个Github issue。 diff --git a/changes/ce/fix-10054.en.md b/changes/ce/fix-10054.en.md deleted file mode 100644 index 5efa73314..000000000 --- a/changes/ce/fix-10054.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix the problem that the obfuscated password is used when using the `/bridges_probe` API to test the connection in Data-Bridge. diff --git a/changes/ce/fix-10054.zh.md b/changes/ce/fix-10054.zh.md deleted file mode 100644 index 45a80dc45..000000000 --- a/changes/ce/fix-10054.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复数据桥接中使用 `/bridges_probe` API 进行测试连接时密码被混淆的问题。 diff --git a/changes/ce/fix-10056.en.md b/changes/ce/fix-10056.en.md deleted file mode 100644 index 55449294d..000000000 --- a/changes/ce/fix-10056.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Fix `/bridges` API status code. -- Return `400` instead of `403` in case of removing a data bridge that is dependent on an active rule. -- Return `400` instead of `403` in case of calling operations (start|stop|restart) when Data-Bridging is not enabled. diff --git a/changes/ce/fix-10056.zh.md b/changes/ce/fix-10056.zh.md deleted file mode 100644 index ec5982137..000000000 --- a/changes/ce/fix-10056.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -修复 `/bridges` API 的 HTTP 状态码。 -- 当删除被活动中的规则依赖的数据桥接时,将返回 `400` 而不是 `403` 。 -- 当数据桥接未启用时,调用操作(启动|停止|重启)将返回 `400` 而不是 `403`。 diff --git a/changes/ce/fix-10058.en.md b/changes/ce/fix-10058.en.md deleted file mode 100644 index 337ac5d47..000000000 --- a/changes/ce/fix-10058.en.md +++ /dev/null @@ -1,7 +0,0 @@ -Deprecate unused QUIC TLS options. -Only following TLS options are kept for the QUIC listeners: - -- cacertfile -- certfile -- keyfile -- verify diff --git a/changes/ce/fix-10058.zh.md b/changes/ce/fix-10058.zh.md deleted file mode 100644 index d1dea37c3..000000000 --- a/changes/ce/fix-10058.zh.md +++ /dev/null @@ -1,8 +0,0 @@ -废弃未使用的 QUIC TLS 选项。 -QUIC 监听器只保留以下 TLS 选项: - -- cacertfile -- certfile -- keyfile -- verify - diff --git a/changes/ce/fix-10066.en.md b/changes/ce/fix-10066.en.md deleted file mode 100644 index 87e253aca..000000000 --- a/changes/ce/fix-10066.en.md +++ /dev/null @@ -1 +0,0 @@ -Improve error messages for `/briges_probe` and `[/node/:node]/bridges/:id/:operation` API calls to make them more readable. And set HTTP status code to `400` instead of `500`. diff --git a/changes/ce/fix-10066.zh.md b/changes/ce/fix-10066.zh.md deleted file mode 100644 index e5e3c2113..000000000 --- a/changes/ce/fix-10066.zh.md +++ /dev/null @@ -1 +0,0 @@ -改进 `/briges_probe` 和 `[/node/:node]/bridges/:id/:operation` API 调用的错误信息,使之更加易读。并将 HTTP 状态代码设置为 `400` 而不是 `500`。 diff --git a/changes/ce/fix-10074.en.md b/changes/ce/fix-10074.en.md deleted file mode 100644 index 49c52b948..000000000 --- a/changes/ce/fix-10074.en.md +++ /dev/null @@ -1 +0,0 @@ -Check if type in `PUT /authorization/sources/:type` matches `type` given in body of request. diff --git a/changes/ce/fix-10074.zh.md b/changes/ce/fix-10074.zh.md deleted file mode 100644 index 930840cdf..000000000 --- a/changes/ce/fix-10074.zh.md +++ /dev/null @@ -1 +0,0 @@ -检查 `PUT /authorization/sources/:type` 中的类型是否与请求正文中的 `type` 相符。 diff --git a/changes/ce/fix-10076.en.md b/changes/ce/fix-10076.en.md deleted file mode 100644 index 5bbbffa32..000000000 --- a/changes/ce/fix-10076.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Fix webhook bridge error handling: connection timeout should be a retriable error. -Prior to this fix, connection timeout was classified as unrecoverable error and led to request being dropped. diff --git a/changes/ce/fix-10076.zh.md b/changes/ce/fix-10076.zh.md deleted file mode 100644 index 516345f92..000000000 --- a/changes/ce/fix-10076.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -修复 HTTP 桥接的一个异常处理:连接超时错误发生后,发生错误的请求可以被重试。 -在此修复前,连接超时后,被当作不可重试类型的错误处理,导致请求被丢弃。 diff --git a/changes/ce/fix-10078.en.md b/changes/ce/fix-10078.en.md deleted file mode 100644 index afb7bcbe0..000000000 --- a/changes/ce/fix-10078.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Fix an issue that invalid QUIC listener setting could casue segfault. - diff --git a/changes/ce/fix-10078.zh.md b/changes/ce/fix-10078.zh.md deleted file mode 100644 index 47a774d1e..000000000 --- a/changes/ce/fix-10078.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -修复了无效的 QUIC 监听器设置可能导致 segfault 的问题。 - diff --git a/changes/ce/fix-10079.en.md b/changes/ce/fix-10079.en.md deleted file mode 100644 index 440351753..000000000 --- a/changes/ce/fix-10079.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix description of `shared_subscription_strategy`. diff --git a/changes/ce/fix-10079.zh.md b/changes/ce/fix-10079.zh.md deleted file mode 100644 index ca2ab9173..000000000 --- a/changes/ce/fix-10079.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -修正对 `shared_subscription_strategy` 的描述。 - diff --git a/changes/ce/fix-10084.en.md b/changes/ce/fix-10084.en.md deleted file mode 100644 index 90da7d660..000000000 --- a/changes/ce/fix-10084.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Fix problem when joining core nodes running different EMQX versions into a cluster. - -[Mria PR](https://github.com/emqx/mria/pull/127) diff --git a/changes/ce/fix-10084.zh.md b/changes/ce/fix-10084.zh.md deleted file mode 100644 index dd44533cf..000000000 --- a/changes/ce/fix-10084.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -修正将运行不同 EMQX 版本的核心节点加入集群的问题。 - -[Mria PR](https://github.com/emqx/mria/pull/127) diff --git a/changes/ce/fix-10085.en.md b/changes/ce/fix-10085.en.md deleted file mode 100644 index e539a04b4..000000000 --- a/changes/ce/fix-10085.en.md +++ /dev/null @@ -1 +0,0 @@ -Consistently return `404` for all requests on non existent source in `/authorization/sources/:source[/*]`. diff --git a/changes/ce/fix-10085.zh.md b/changes/ce/fix-10085.zh.md deleted file mode 100644 index 059680efa..000000000 --- a/changes/ce/fix-10085.zh.md +++ /dev/null @@ -1 +0,0 @@ -如果向 `/authorization/sources/:source[/*]` 请求的 `source` 不存在,将一致地返回 `404`。 diff --git a/changes/ce/fix-10086.en.md b/changes/ce/fix-10086.en.md deleted file mode 100644 index d337a57c7..000000000 --- a/changes/ce/fix-10086.en.md +++ /dev/null @@ -1,4 +0,0 @@ -Upgrade HTTP client ehttpc to `0.4.7`. -Prior to this upgrade, HTTP clients for authentication, authorization and webhook may crash -if `Body` is empty but `Content-Type` HTTP header is set. -For more details see [ehttpc PR#44](https://github.com/emqx/ehttpc/pull/44). diff --git a/changes/ce/fix-10086.zh.md b/changes/ce/fix-10086.zh.md deleted file mode 100644 index c083d6055..000000000 --- a/changes/ce/fix-10086.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -HTTP 客户端库 `ehttpc` 升级到 0.4.7。 -在升级前,如果 HTTP 客户端,例如 '认证'、'授权'、'WebHook' 等配置中使用了 `Content-Type` HTTP 头,但是没有配置 `Body`,则可能会发生异常。 -详情见 [ehttpc PR#44](https://github.com/emqx/ehttpc/pull/44)。 diff --git a/changes/ce/fix-10098.en.md b/changes/ce/fix-10098.en.md deleted file mode 100644 index 61058da0a..000000000 --- a/changes/ce/fix-10098.en.md +++ /dev/null @@ -1 +0,0 @@ -A crash with an error in the log file that happened when the MongoDB authorization module queried the database has been fixed. diff --git a/changes/ce/fix-10100.en.md b/changes/ce/fix-10100.en.md deleted file mode 100644 index e16ee5efc..000000000 --- a/changes/ce/fix-10100.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Fix channel crash for slow clients with enhanced authentication. -Previously, when the client was using enhanced authentication, but the Auth message was sent slowly or the Auth message was lost, the client process would crash. diff --git a/changes/ce/fix-10100.zh.md b/changes/ce/fix-10100.zh.md deleted file mode 100644 index ac2483a27..000000000 --- a/changes/ce/fix-10100.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -修复响应较慢的客户端在使用增强认证时可能出现崩溃的问题。 -此前,当客户端使用增强认证功能,但发送 Auth 报文较慢或 Auth 报文丢失时会导致客户端进程崩溃。 diff --git a/changes/ce/fix-10107.en.md b/changes/ce/fix-10107.en.md deleted file mode 100644 index 1bcbbad60..000000000 --- a/changes/ce/fix-10107.en.md +++ /dev/null @@ -1,9 +0,0 @@ -For operations on `bridges API` if `bridge-id` is unknown we now return `404` -instead of `400`. Also a bug was fixed that caused a crash if that was a node -operation. Additionally we now also check if the given bridge is enabled when -doing the cluster operation `start` . Affected endpoints: - * [cluster] `/bridges/:id/:operation`, - * [node] `/nodes/:node/bridges/:id/:operation`, where `operation` is one of -`[start|stop|restart]`. -Moreover, for a node operation, EMQX checks if node name is in our cluster and -return `404` instead of `501`. diff --git a/changes/ce/fix-10107.zh.md b/changes/ce/fix-10107.zh.md deleted file mode 100644 index e541a834f..000000000 --- a/changes/ce/fix-10107.zh.md +++ /dev/null @@ -1,8 +0,0 @@ -现在对桥接的 API 进行调用时,如果 `bridge-id` 不存在,将会返回 `404`,而不再是`400`。 -然后,还修复了这种情况下,在节点级别上进行 API 调用时,可能导致崩溃的问题。 -另外,在启动某个桥接时,会先检查指定桥接是否已启用。 -受影响的接口有: - * [cluster] `/bridges/:id/:operation`, - * [node] `/nodes/:node/bridges/:id/:operation`, -其中 `operation` 是 `[start|stop|restart]` 之一。 -此外,对于节点操作,EMQX 将检查节点是否存在于集群中,如果不在,则会返回`404`,而不再是`501`。 diff --git a/changes/ce/fix-10118.en.md b/changes/ce/fix-10118.en.md deleted file mode 100644 index f6db758f3..000000000 --- a/changes/ce/fix-10118.en.md +++ /dev/null @@ -1,4 +0,0 @@ -Fix problems related to manual joining of EMQX replicant nodes to the cluster. -Previously, after manually executing joining and then leaving the cluster, the `replicant` node can only run normally after restarting the node after joining the cluster again. - -[Mria PR](https://github.com/emqx/mria/pull/128) diff --git a/changes/ce/fix-10118.zh.md b/changes/ce/fix-10118.zh.md deleted file mode 100644 index a037215f0..000000000 --- a/changes/ce/fix-10118.zh.md +++ /dev/null @@ -1,4 +0,0 @@ -修复 `replicant` 节点因为手动加入 EMQX 集群导致的相关问题。 -此前,手动执行 `加入集群-离开集群` 后,`replicant` 节点再次加入集群后只有重启节点才能正常运行。 - -[Mria PR](https://github.com/emqx/mria/pull/128) diff --git a/changes/ce/fix-10119.en.md b/changes/ce/fix-10119.en.md deleted file mode 100644 index c23a9dcdb..000000000 --- a/changes/ce/fix-10119.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix crash when `statsd.server` is set to an empty string. diff --git a/changes/ce/fix-10119.zh.md b/changes/ce/fix-10119.zh.md deleted file mode 100644 index c77b99025..000000000 --- a/changes/ce/fix-10119.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 `statsd.server` 配置为空字符串时启动崩溃的问题。 diff --git a/changes/ce/fix-10124.en.md b/changes/ce/fix-10124.en.md deleted file mode 100644 index 1a4aca3d9..000000000 --- a/changes/ce/fix-10124.en.md +++ /dev/null @@ -1 +0,0 @@ -The default heartbeat period for MongoDB has been increased to reduce the risk of too excessive logging to the MongoDB log file. diff --git a/changes/ce/fix-10130.en.md b/changes/ce/fix-10130.en.md deleted file mode 100644 index 98484e38f..000000000 --- a/changes/ce/fix-10130.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Fix garbled config display in dashboard when the value is originally from environment variables. -For example, `env EMQX_STATSD__SERVER='127.0.0.1:8124' . /bin/emqx start` results in unreadable string (not '127.0.0.1:8124') displayed in Dashboard's Statsd settings page. -Related PR: [HOCON#234](https://github.com/emqx/hocon/pull/234). diff --git a/changes/ce/fix-10130.zh.md b/changes/ce/fix-10130.zh.md deleted file mode 100644 index 19c092fdf..000000000 --- a/changes/ce/fix-10130.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -修复通过环境变量配置启动的 EMQX 节点无法通过HTTP API获取到正确的配置信息。 -比如:`EMQX_STATSD__SERVER='127.0.0.1:8124' ./bin/emqx start` 后通过 Dashboard看到的 Statsd 配置信息是乱码。 -相关 PR: [HOCON:234](https://github.com/emqx/hocon/pull/234). diff --git a/changes/ce/fix-10145.en.md b/changes/ce/fix-10145.en.md new file mode 100644 index 000000000..eaa896793 --- /dev/null +++ b/changes/ce/fix-10145.en.md @@ -0,0 +1,3 @@ +Fix `bridges` API to report error conditions for a failing bridge as +`status_reason`. Also when creating an alarm for a failing resource we include +this error condition with the alarm's message. diff --git a/changes/ce/fix-10154.en.md b/changes/ce/fix-10154.en.md new file mode 100644 index 000000000..24bc4bae1 --- /dev/null +++ b/changes/ce/fix-10154.en.md @@ -0,0 +1,8 @@ +Change the default `resume_interval` for bridges and connectors to be +the minimum of `health_check_interval` and `request_timeout / 3`. +Also exposes it as a hidden configuration to allow fine tuning. + +Before this change, the default values for `resume_interval` meant +that, if a buffer ever got blocked due to resource errors or high +message volumes, then, by the time the buffer would try to resume its +normal operations, almost all requests would have timed out. diff --git a/changes/ce/fix-10172.en.md b/changes/ce/fix-10172.en.md new file mode 100644 index 000000000..d5cec50f8 --- /dev/null +++ b/changes/ce/fix-10172.en.md @@ -0,0 +1,9 @@ +Fix the incorrect default ACL rule, which was: +``` +{allow, {username, "^dashboard?"}, subscribe, ["$SYS/#"]}. +``` + +However, it should use `{re, "^dashboard$"}` to perform a regular expression match: +``` +{allow, {username, {re,"^dashboard$"}}, subscribe, ["$SYS/#"]}. +``` diff --git a/changes/ce/fix-10172.zh.md b/changes/ce/fix-10172.zh.md new file mode 100644 index 000000000..bfdfab60c --- /dev/null +++ b/changes/ce/fix-10172.zh.md @@ -0,0 +1,8 @@ +修复错误的默认 ACL 规则,之前是: +``` +{allow, {username, "^dashboard?"}, subscribe, ["$SYS/#"]}. +``` +但执行正则表达式的匹配应该使用 `{re, "^dashboard$"}`: +``` +{allow, {username, {re, "^dashboard$"}}, subscribe, ["$SYS/#"]}. +``` diff --git a/changes/ce/fix-10174.en.md b/changes/ce/fix-10174.en.md new file mode 100644 index 000000000..213af19da --- /dev/null +++ b/changes/ce/fix-10174.en.md @@ -0,0 +1,2 @@ +Upgrade library `esockd` from 5.9.4 to 5.9.6. +Fix an unnecessary error level logging when a connection is closed before proxy protocol header is sent by the proxy. diff --git a/changes/ce/fix-10174.zh.md b/changes/ce/fix-10174.zh.md new file mode 100644 index 000000000..435056280 --- /dev/null +++ b/changes/ce/fix-10174.zh.md @@ -0,0 +1,2 @@ +依赖库 `esockd` 从 5.9.4 升级到 5.9.6。 +修复了一个不必要的错误日志。如果连接在 proxy protocol 包头还没有发送前就关闭了, 则不打印错误日志。 diff --git a/changes/ce/fix-10195.en.md b/changes/ce/fix-10195.en.md new file mode 100644 index 000000000..35cc7c082 --- /dev/null +++ b/changes/ce/fix-10195.en.md @@ -0,0 +1 @@ +Add labels to API schemas where description contains HTML and breaks formatting of generated documentation otherwise. diff --git a/changes/ce/fix-10196.en.md b/changes/ce/fix-10196.en.md new file mode 100644 index 000000000..58ff01d8e --- /dev/null +++ b/changes/ce/fix-10196.en.md @@ -0,0 +1 @@ +Use lower-case for schema summaries and descritptions to be used in menu of generated online documentation. diff --git a/changes/ce/fix-10209.en.md b/changes/ce/fix-10209.en.md new file mode 100644 index 000000000..21ce98e44 --- /dev/null +++ b/changes/ce/fix-10209.en.md @@ -0,0 +1,2 @@ +Fix bug where a last will testament (LWT) message could be published +when kicking out a banned client. diff --git a/changes/ce/fix-10211.en.md b/changes/ce/fix-10211.en.md new file mode 100644 index 000000000..9474f2027 --- /dev/null +++ b/changes/ce/fix-10211.en.md @@ -0,0 +1,3 @@ +Hide `broker.broker_perf` config and API documents. +The two configs `route_lock_type` and `trie_compaction` are rarely used and requires a full cluster restart to take effect. They are not suitable for being exposed to users. +Detailed changes can be found here: https://gist.github.com/zmstone/01ad5754b9beaeaf3f5b86d14d49a0b7/revisions diff --git a/changes/ce/fix-10211.zh.md b/changes/ce/fix-10211.zh.md new file mode 100644 index 000000000..e8db64f86 --- /dev/null +++ b/changes/ce/fix-10211.zh.md @@ -0,0 +1,3 @@ +隐藏 `broker.broker_perf` 配置项,不再在 配置和 API 的文档中展示。 +`route_lock_type` 和 `trie_compaction` 这两个配置项很少使用,且需要全集群重启才能生效,不适合暴露给用户。 +详细对比: https://gist.github.com/zmstone/01ad5754b9beaeaf3f5b86d14d49a0b7/revisions diff --git a/changes/ce/fix-10225.en.md b/changes/ce/fix-10225.en.md new file mode 100644 index 000000000..20f7dfa47 --- /dev/null +++ b/changes/ce/fix-10225.en.md @@ -0,0 +1,2 @@ +Allow installing a plugin if its name matches the beginning of another (already installed) plugin name. +For example: if plugin "emqx_plugin_template_a" is installed, it must not block installing plugin "emqx_plugin_template". diff --git a/changes/ce/fix-10226.en.md b/changes/ce/fix-10226.en.md new file mode 100644 index 000000000..2d833d2dc --- /dev/null +++ b/changes/ce/fix-10226.en.md @@ -0,0 +1 @@ +Don't crash on validation error in `/bridges` API, return `400` instead. diff --git a/changes/ce/fix-10237.en.md b/changes/ce/fix-10237.en.md new file mode 100644 index 000000000..cf3fc707b --- /dev/null +++ b/changes/ce/fix-10237.en.md @@ -0,0 +1 @@ +Ensure we return `404` status code for unknown node names in `/nodes/:node[/metrics|/stats]` API. diff --git a/changes/ce/fix-10242.en.md b/changes/ce/fix-10242.en.md new file mode 100644 index 000000000..bc4309b94 --- /dev/null +++ b/changes/ce/fix-10242.en.md @@ -0,0 +1,2 @@ +Fixed a log data field name clash. +Piror to this fix, some debug logs may report a wrong Erlang PID which may affect troubleshooting session takeover issues. diff --git a/changes/ce/fix-10242.zh.md b/changes/ce/fix-10242.zh.md new file mode 100644 index 000000000..36c0a1556 --- /dev/null +++ b/changes/ce/fix-10242.zh.md @@ -0,0 +1,2 @@ +修复log数据字段名称冲突。 +在这个修复之前,一些调试日志可能会报告一个错误的Erlang PID,这可能会影响会话接管问题的故障调查。 diff --git a/changes/ce/fix-10257.en.md b/changes/ce/fix-10257.en.md new file mode 100644 index 000000000..aa5ed2519 --- /dev/null +++ b/changes/ce/fix-10257.en.md @@ -0,0 +1,11 @@ +Fixed the issue where `auto_observe` was not working in LwM2M Gateway. + +Before the fix, OBSERVE requests were sent without a token, causing failures +that LwM2M clients could not handle. + +After the fix, LwM2M Gateway can correctly observe the resource list carried by +client, furthermore, unknown resources will be ignored and printing the following +warning log: +``` +2023-03-28T18:50:27.771123+08:00 [warning] msg: ignore_observer_resource, mfa: emqx_lwm2m_session:observe_object_list/3, line: 522, peername: 127.0.0.1:56830, clientid: testlwm2mclient, object_id: 31024, reason: no_xml_definition +``` diff --git a/changes/ce/fix-10257.zh.md b/changes/ce/fix-10257.zh.md new file mode 100644 index 000000000..962495f2d --- /dev/null +++ b/changes/ce/fix-10257.zh.md @@ -0,0 +1,8 @@ +修复 LwM2M 网关 `auto_observe` 不工作的问题。 + +在修复之前,下发的 OBSERVE 请求没有 Token 从而导致 LwM2M 客户端无法处理的失败。 + +修复后,能正确监听 LwM2M 携带的资源列表、和会忽略未知的资源,并打印以下日志: +``` +2023-03-28T18:50:27.771123+08:00 [warning] msg: ignore_observer_resource, mfa: emqx_lwm2m_session:observe_object_list/3, line: 522, peername: 127.0.0.1:56830, clientid: testlwm2mclient, object_id: 31024, reason: no_xml_definition +``` diff --git a/changes/ce/fix-10286.en.md b/changes/ce/fix-10286.en.md new file mode 100644 index 000000000..3a51fefe2 --- /dev/null +++ b/changes/ce/fix-10286.en.md @@ -0,0 +1,2 @@ +Enhance logging behaviour during boot failure. +When EMQX fails to start due to corrupted configuration files, excessive logging is eliminated and no crash dump file is generated. diff --git a/changes/ce/fix-10286.zh.md b/changes/ce/fix-10286.zh.md new file mode 100644 index 000000000..83455b8fd --- /dev/null +++ b/changes/ce/fix-10286.zh.md @@ -0,0 +1,2 @@ +优化启动失败的错误日志。 +如果 EMQX 因为损坏的配置文件无法启动时,不会再打印过多的错误日志,也不再生成 crash.dump 文件。 diff --git a/changes/ce/fix-10297.en.md b/changes/ce/fix-10297.en.md new file mode 100644 index 000000000..305473b22 --- /dev/null +++ b/changes/ce/fix-10297.en.md @@ -0,0 +1 @@ +Keeps `eval` command backward compatible with v4 by evaluating only Erlang expressions, even on Elixir node. For Elixir expressions, use `eval-ex` command. diff --git a/changes/ce/fix-10300.en.md b/changes/ce/fix-10300.en.md new file mode 100644 index 000000000..a60f9dfcd --- /dev/null +++ b/changes/ce/fix-10300.en.md @@ -0,0 +1 @@ +Fixed an issue where a build made with Elixir could not receive uploaded plugins until the `plugins` folder was created manually to receive the uploaded files. diff --git a/changes/ce/fix-9939.en.md b/changes/ce/fix-9939.en.md deleted file mode 100644 index 83e84c493..000000000 --- a/changes/ce/fix-9939.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Allow 'emqx ctl cluster' command to be issued before Mnesia starts. -Prior to this change, EMQX `replicant` could not use `manual` discovery strategy. -Now it's possible to join cluster using 'manual' strategy. diff --git a/changes/ce/fix-9939.zh.md b/changes/ce/fix-9939.zh.md deleted file mode 100644 index 4b150c5fc..000000000 --- a/changes/ce/fix-9939.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -允许 'emqx ctl cluster join' 命令在 Mnesia 启动前就可以调用。 -在此修复前, EMQX 的 `replicant` 类型节点无法使用 `manual` 集群发现策略。 diff --git a/changes/ce/fix-9958.en.md b/changes/ce/fix-9958.en.md deleted file mode 100644 index 821934ad0..000000000 --- a/changes/ce/fix-9958.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix bad http response format when client ID is not found in `clients` APIs diff --git a/changes/ce/fix-9958.zh.md b/changes/ce/fix-9958.zh.md deleted file mode 100644 index a26fbb7fe..000000000 --- a/changes/ce/fix-9958.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 `clients` API 在 Client ID 不存在时返回的错误的 HTTP 应答格式。 diff --git a/changes/ce/fix-9961.en.md b/changes/ce/fix-9961.en.md deleted file mode 100644 index 6185a64ea..000000000 --- a/changes/ce/fix-9961.en.md +++ /dev/null @@ -1 +0,0 @@ -Avoid parsing config files for node name and cookie when executing non-boot commands in bin/emqx. diff --git a/changes/ce/fix-9961.zh.md b/changes/ce/fix-9961.zh.md deleted file mode 100644 index edd90b2ca..000000000 --- a/changes/ce/fix-9961.zh.md +++ /dev/null @@ -1 +0,0 @@ -在 bin/emqx 脚本中,避免在运行非启动命令时解析 emqx.conf 来获取节点名称和 cookie。 diff --git a/changes/ce/fix-9974.en.md b/changes/ce/fix-9974.en.md deleted file mode 100644 index 97223e03f..000000000 --- a/changes/ce/fix-9974.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Report memory usage to statsd and prometheus using the same data source as dashboard. -Prior to this fix, the memory usage data source was collected from an outdated source which did not work well in containers. diff --git a/changes/ce/fix-9974.zh.md b/changes/ce/fix-9974.zh.md deleted file mode 100644 index 8358204f3..000000000 --- a/changes/ce/fix-9974.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -Statsd 和 prometheus 使用跟 Dashboard 相同的内存用量数据源。 -在此修复前,内存的总量和用量统计使用了过时的(在容器环境中不准确)的数据源。 diff --git a/changes/ce/fix-9978.en.md b/changes/ce/fix-9978.en.md deleted file mode 100644 index 6750d136f..000000000 --- a/changes/ce/fix-9978.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Fixed configuration issue when choosing to use SSL for a Postgres connection (`authn`, `authz` and bridge). -The connection could fail to complete with a previously working configuration after an upgrade from 5.0.13 to newer EMQX versions. diff --git a/changes/ce/fix-9978.zh.md b/changes/ce/fix-9978.zh.md deleted file mode 100644 index 75eed3600..000000000 --- a/changes/ce/fix-9978.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -修正了在Postgres连接中选择使用SSL时的配置问题(`authn`, `authz` 和 bridge)。 -从5.0.13升级到较新的EMQX版本后,连接可能无法完成之前的配置。 diff --git a/changes/ce/fix-9997.en.md b/changes/ce/fix-9997.en.md deleted file mode 100644 index be0344ec1..000000000 --- a/changes/ce/fix-9997.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix Swagger API schema generation. `deprecated` metadata field is now always boolean, as [Swagger specification](https://swagger.io/specification/) suggests. diff --git a/changes/ce/fix-9997.zh.md b/changes/ce/fix-9997.zh.md deleted file mode 100644 index 6f1a0b779..000000000 --- a/changes/ce/fix-9997.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 Swagger API 生成时,`deprecated` 元数据字段未按照[标准](https://swagger.io/specification/)建议的那样始终为布尔值的问题。 diff --git a/changes/ce/perf-9967.en.md b/changes/ce/perf-9967.en.md deleted file mode 100644 index fadba24c9..000000000 --- a/changes/ce/perf-9967.en.md +++ /dev/null @@ -1 +0,0 @@ -New common TLS option 'hibernate_after' to reduce memory footprint per idle connecion, default: 5s. diff --git a/changes/ce/perf-9967.zh.md b/changes/ce/perf-9967.zh.md deleted file mode 100644 index 7b73f9bd0..000000000 --- a/changes/ce/perf-9967.zh.md +++ /dev/null @@ -1 +0,0 @@ -新的通用 TLS 选项 'hibernate_after', 以减少空闲连接的内存占用,默认: 5s 。 diff --git a/changes/ce/perf-9998.en.md b/changes/ce/perf-9998.en.md deleted file mode 100644 index e9e23a25e..000000000 --- a/changes/ce/perf-9998.en.md +++ /dev/null @@ -1 +0,0 @@ -Redact the HTTP request body in the authentication error logs for security reasons. diff --git a/changes/ce/perf-9998.zh.md b/changes/ce/perf-9998.zh.md deleted file mode 100644 index 146eb858f..000000000 --- a/changes/ce/perf-9998.zh.md +++ /dev/null @@ -1 +0,0 @@ -出于安全原因,在身份验证错误日志中模糊 HTTP 请求正文。 diff --git a/changes/ee/feat-10083.en.md b/changes/ee/feat-10083.en.md deleted file mode 100644 index f4331faf9..000000000 --- a/changes/ee/feat-10083.en.md +++ /dev/null @@ -1 +0,0 @@ -Add `DynamoDB` support for Data-Brdige. diff --git a/changes/ee/feat-10083.zh.md b/changes/ee/feat-10083.zh.md deleted file mode 100644 index 8274e62c2..000000000 --- a/changes/ee/feat-10083.zh.md +++ /dev/null @@ -1 +0,0 @@ -为数据桥接增加 `DynamoDB` 支持。 diff --git a/changes/ee/feat-10140.en.md b/changes/ee/feat-10140.en.md new file mode 100644 index 000000000..f2616fda1 --- /dev/null +++ b/changes/ee/feat-10140.en.md @@ -0,0 +1,4 @@ +Integrate `Cassandra` into `bridges` as a new backend. At the current stage: +- Only support Cassandra version 3.x, not yet 4.x. +- Only support storing data in synchronously, not yet asynchronous and batch + method to store data to Cassandra. diff --git a/changes/ee/feat-10140.zh.md b/changes/ee/feat-10140.zh.md new file mode 100644 index 000000000..5b070133a --- /dev/null +++ b/changes/ee/feat-10140.zh.md @@ -0,0 +1,3 @@ +支持 Cassandra 数据桥接。在当前阶段: +- 仅支持 Cassandra 3.x 版本,暂不支持 4.x。 +- 仅支持以同步的方式存储数据,暂不支持异步和批量的方式来存储数据到 Cassandra。 diff --git a/changes/ee/feat-10143.en.md b/changes/ee/feat-10143.en.md new file mode 100644 index 000000000..67fc13dc2 --- /dev/null +++ b/changes/ee/feat-10143.en.md @@ -0,0 +1 @@ +Add `RocketMQ` data integration bridge. diff --git a/changes/ee/feat-10143.zh.md b/changes/ee/feat-10143.zh.md new file mode 100644 index 000000000..85a13ffa7 --- /dev/null +++ b/changes/ee/feat-10143.zh.md @@ -0,0 +1 @@ +为数据桥接增加 `RocketMQ` 支持。 diff --git a/changes/ee/feat-10165.en.md b/changes/ee/feat-10165.en.md new file mode 100644 index 000000000..199d45707 --- /dev/null +++ b/changes/ee/feat-10165.en.md @@ -0,0 +1,2 @@ +Support escaped special characters in InfluxDB data bridge write_syntax. +This update allows to use escaped special characters in string elements in accordance with InfluxDB line protocol. diff --git a/changes/ee/feat-10294.en.md b/changes/ee/feat-10294.en.md new file mode 100644 index 000000000..cac3a7587 --- /dev/null +++ b/changes/ee/feat-10294.en.md @@ -0,0 +1 @@ +When configuring a MongoDB bridge, you can now use the ${var} syntax to reference fields in the message payload within the collection field. This enables you to select the collection to insert data into dynamically. diff --git a/changes/ee/feat-10294.zh.md b/changes/ee/feat-10294.zh.md new file mode 100644 index 000000000..ca1727012 --- /dev/null +++ b/changes/ee/feat-10294.zh.md @@ -0,0 +1 @@ +在配置 MongoDB 桥时,现在可以使用 ${var} 语法来引用消息负载中的字段,以便动态选择要插入的集合。 diff --git a/changes/ee/feat-9564.en.md b/changes/ee/feat-9564.en.md deleted file mode 100644 index 4405e3e07..000000000 --- a/changes/ee/feat-9564.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Implemented Kafka Consumer bridge. -Now it's possible to consume messages from Kafka and publish them to MQTT topics. diff --git a/changes/ee/feat-9564.zh.md b/changes/ee/feat-9564.zh.md deleted file mode 100644 index 01a7ffe58..000000000 --- a/changes/ee/feat-9564.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -实现了 Kafka 消费者桥接。 -现在可以从 Kafka 消费消息并将其发布到 MQTT 主题。 diff --git a/changes/ee/feat-9881.en.md b/changes/ee/feat-9881.en.md deleted file mode 100644 index 546178965..000000000 --- a/changes/ee/feat-9881.en.md +++ /dev/null @@ -1,4 +0,0 @@ -In this pull request, we have enhanced the error logs related to InfluxDB connectivity health checks. -Previously, if InfluxDB failed to pass the health checks using the specified parameters, the only message provided was "timed out waiting for it to become healthy". -With the updated implementation, the error message will be displayed in both the logs and the dashboard, enabling easier identification and resolution of the issue. - diff --git a/changes/ee/feat-9881.zh.md b/changes/ee/feat-9881.zh.md deleted file mode 100644 index 9746a4c0a..000000000 --- a/changes/ee/feat-9881.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -增强了与 InfluxDB 连接健康检查相关的错误日志。 -在此更改之前,如果使用配置的参数 InfluxDB 未能通过健康检查,用户仅能获得一个“超时”的信息。 -现在,详细的错误消息将显示在日志和控制台,从而让用户更容易地识别和解决问题。 diff --git a/changes/ee/feat-9932.en.md b/changes/ee/feat-9932.en.md deleted file mode 100644 index f4f9ce40d..000000000 --- a/changes/ee/feat-9932.en.md +++ /dev/null @@ -1 +0,0 @@ -Integrate `TDengine` into `bridges` as a new backend. diff --git a/changes/ee/feat-9932.zh.md b/changes/ee/feat-9932.zh.md deleted file mode 100644 index 1fbf7bf34..000000000 --- a/changes/ee/feat-9932.zh.md +++ /dev/null @@ -1 +0,0 @@ -在 `桥接` 中集成 `TDengine`。 diff --git a/changes/ee/fix-10007.en.md b/changes/ee/fix-10007.en.md deleted file mode 100644 index 1adab8e9b..000000000 --- a/changes/ee/fix-10007.en.md +++ /dev/null @@ -1,5 +0,0 @@ -Change Kafka bridge's config `memory_overload_protection` default value from `true` to `false`. -EMQX logs cases when messages get dropped due to overload protection, and this is also reflected in counters. -However, since there is by default no alerting based on the logs and counters, -setting it to `true` may cause messages being dropped without noticing. -At the time being, the better option is to let sysadmin set it explicitly so they are fully aware of the benefits and risks. diff --git a/changes/ee/fix-10007.zh.md b/changes/ee/fix-10007.zh.md deleted file mode 100644 index 0c08f20d0..000000000 --- a/changes/ee/fix-10007.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -Kafka 桥接的配置参数 `memory_overload_protection` 默认值从 `true` 改成了 `false`。 -尽管内存过载后消息被丢弃会产生日志和计数,如果没有基于这些日志或计数的告警,系统管理员可能无法及时发现消息被丢弃。 -当前更好的选择是:让管理员显式的配置该项,迫使他们理解这个配置的好处以及风险。 diff --git a/changes/ee/fix-10087.en.md b/changes/ee/fix-10087.en.md deleted file mode 100644 index fd6e10b7b..000000000 --- a/changes/ee/fix-10087.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Use default template `${timestamp}` if the `timestamp` config is empty (undefined) when inserting data in InfluxDB. -Prior to this change, InfluxDB bridge inserted a wrong timestamp when template is not provided. diff --git a/changes/ee/fix-10087.zh.md b/changes/ee/fix-10087.zh.md deleted file mode 100644 index e08e61f37..000000000 --- a/changes/ee/fix-10087.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -在 InfluxDB 中插入数据时,如果时间戳为空(未定义),则使用默认的占位符 `${timestamp}`。 -在此修复前,如果时间戳字段没有设置,InfluxDB 桥接使用了一个错误的时间戳。 diff --git a/changes/ee/fix-10095.en.md b/changes/ee/fix-10095.en.md deleted file mode 100644 index 49c588345..000000000 --- a/changes/ee/fix-10095.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Stop MySQL client from bombarding server repeatedly with unnecessary `PREPARE` queries on every batch, trashing the server and exhausting its internal limits. This was happening when the MySQL bridge was in the batch mode. - -Ensure safer and more careful escaping of strings and binaries in batch insert queries when the MySQL bridge is in the batch mode. diff --git a/changes/ee/fix-10201.en.md b/changes/ee/fix-10201.en.md new file mode 100644 index 000000000..b3dd53150 --- /dev/null +++ b/changes/ee/fix-10201.en.md @@ -0,0 +1 @@ +In TDengine, removed the redundant database name from the SQL template. diff --git a/changes/ee/fix-10201.zh.md b/changes/ee/fix-10201.zh.md new file mode 100644 index 000000000..53b175551 --- /dev/null +++ b/changes/ee/fix-10201.zh.md @@ -0,0 +1 @@ +在 TDengine 桥接的 SQL 模板中,删除了多余的数据库表名。 diff --git a/changes/ee/fix-10270.en.md b/changes/ee/fix-10270.en.md new file mode 100644 index 000000000..65eed7b5d --- /dev/null +++ b/changes/ee/fix-10270.en.md @@ -0,0 +1 @@ +Clickhouse has got a fix that makes the error message better when users click the test button in the settings dialog. diff --git a/changes/ee/fix-10270.zh.md b/changes/ee/fix-10270.zh.md new file mode 100644 index 000000000..d47278c16 --- /dev/null +++ b/changes/ee/fix-10270.zh.md @@ -0,0 +1 @@ +Clickhouse 已经修复了一个问题,当用户在设置对话框中点击测试按钮时,错误信息会更清晰。 diff --git a/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml b/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml index 00751aceb..3e9e39f2c 100644 --- a/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml +++ b/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml @@ -74,9 +74,9 @@ spec: secret: secretName: {{ .Values.emqxLicenseSecretName }} {{- end }} - {{- if .Values.extraVolumes }} - {{- toYaml .Values.extraVolumes | nindent 8 }} - {{- end }} + {{- if .Values.extraVolumes }} + {{- toYaml .Values.extraVolumes | nindent 6 }} + {{- end }} {{- if .Values.podSecurityContext.enabled }} securityContext: {{- omit .Values.podSecurityContext "enabled" | toYaml | nindent 8 }} {{- end }} @@ -141,9 +141,9 @@ spec: subPath: "emqx.lic" readOnly: true {{- end }} - {{- if .Values.extraVolumeMounts }} - {{- toYaml .Values.extraVolumeMounts | nindent 12 }} - {{- end }} + {{- if .Values.extraVolumeMounts }} + {{- toYaml .Values.extraVolumeMounts | nindent 10 }} + {{- end }} readinessProbe: httpGet: path: /status diff --git a/deploy/charts/emqx-enterprise/templates/service.yaml b/deploy/charts/emqx-enterprise/templates/service.yaml index 401746a51..dea548653 100644 --- a/deploy/charts/emqx-enterprise/templates/service.yaml +++ b/deploy/charts/emqx-enterprise/templates/service.yaml @@ -114,7 +114,7 @@ metadata: spec: type: ClusterIP sessionAffinity: None - clusterIP: None + clusterIP: {{ .Values.service.clusterIP | default "None" }} publishNotReadyAddresses: true ports: - name: mqtt diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 00751aceb..3e9e39f2c 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -74,9 +74,9 @@ spec: secret: secretName: {{ .Values.emqxLicenseSecretName }} {{- end }} - {{- if .Values.extraVolumes }} - {{- toYaml .Values.extraVolumes | nindent 8 }} - {{- end }} + {{- if .Values.extraVolumes }} + {{- toYaml .Values.extraVolumes | nindent 6 }} + {{- end }} {{- if .Values.podSecurityContext.enabled }} securityContext: {{- omit .Values.podSecurityContext "enabled" | toYaml | nindent 8 }} {{- end }} @@ -141,9 +141,9 @@ spec: subPath: "emqx.lic" readOnly: true {{- end }} - {{- if .Values.extraVolumeMounts }} - {{- toYaml .Values.extraVolumeMounts | nindent 12 }} - {{- end }} + {{- if .Values.extraVolumeMounts }} + {{- toYaml .Values.extraVolumeMounts | nindent 10 }} + {{- end }} readinessProbe: httpGet: path: /status diff --git a/deploy/charts/emqx/templates/service.yaml b/deploy/charts/emqx/templates/service.yaml index 401746a51..dea548653 100644 --- a/deploy/charts/emqx/templates/service.yaml +++ b/deploy/charts/emqx/templates/service.yaml @@ -114,7 +114,7 @@ metadata: spec: type: ClusterIP sessionAffinity: None - clusterIP: None + clusterIP: {{ .Values.service.clusterIP | default "None" }} publishNotReadyAddresses: true ports: - name: mqtt diff --git a/deploy/charts/emqx/values.yaml b/deploy/charts/emqx/values.yaml index f4649cc15..f7c6483fe 100644 --- a/deploy/charts/emqx/values.yaml +++ b/deploy/charts/emqx/values.yaml @@ -128,6 +128,9 @@ service: ## Service type ## type: ClusterIP + ## The cluster IP if one wants to customize it to a fixed value + ## + clusterIP: None ## Port for MQTT ## mqtt: 1883 diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index bb5c23ea4..8f04c433c 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG BUILD_FROM=ghcr.io/emqx/emqx-builder/5.0-28:1.13.4-24.3.4.2-2-debian11 +ARG BUILD_FROM=ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-debian11 ARG RUN_FROM=debian:11-slim FROM ${BUILD_FROM} AS builder diff --git a/lib-ee/emqx_ee_bridge/docker-ct b/lib-ee/emqx_ee_bridge/docker-ct index ac1728ad2..963122082 100644 --- a/lib-ee/emqx_ee_bridge/docker-ct +++ b/lib-ee/emqx_ee_bridge/docker-ct @@ -10,3 +10,5 @@ pgsql tdengine clickhouse dynamo +rocketmq +cassandra diff --git a/lib-ee/emqx_ee_bridge/rebar.config b/lib-ee/emqx_ee_bridge/rebar.config index be0cb5345..985b387d4 100644 --- a/lib-ee/emqx_ee_bridge/rebar.config +++ b/lib-ee/emqx_ee_bridge/rebar.config @@ -3,6 +3,7 @@ , {kafka_protocol, {git, "https://github.com/kafka4beam/kafka_protocol.git", {tag, "4.1.2"}}} , {brod_gssapi, {git, "https://github.com/kafka4beam/brod_gssapi.git", {tag, "v0.1.0-rc1"}}} , {brod, {git, "https://github.com/kafka4beam/brod.git", {tag, "3.16.8"}}} + , {ecql, {git, "https://github.com/emqx/ecql.git", {tag, "v0.4.2"}}} , {emqx_connector, {path, "../../apps/emqx_connector"}} , {emqx_resource, {path, "../../apps/emqx_resource"}} , {emqx_bridge, {path, "../../apps/emqx_bridge"}} diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl index ec81b7935..1ddc1a110 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl @@ -32,7 +32,9 @@ api_schemas(Method) -> ref(emqx_ee_bridge_matrix, Method), ref(emqx_ee_bridge_tdengine, Method), ref(emqx_ee_bridge_clickhouse, Method), - ref(emqx_ee_bridge_dynamo, Method) + ref(emqx_ee_bridge_dynamo, Method), + ref(emqx_ee_bridge_rocketmq, Method), + ref(emqx_ee_bridge_cassa, Method) ]. schema_modules() -> @@ -49,7 +51,9 @@ schema_modules() -> emqx_ee_bridge_matrix, emqx_ee_bridge_tdengine, emqx_ee_bridge_clickhouse, - emqx_ee_bridge_dynamo + emqx_ee_bridge_dynamo, + emqx_ee_bridge_rocketmq, + emqx_ee_bridge_cassa ]. examples(Method) -> @@ -85,7 +89,9 @@ resource_type(timescale) -> emqx_connector_pgsql; resource_type(matrix) -> emqx_connector_pgsql; resource_type(tdengine) -> emqx_ee_connector_tdengine; resource_type(clickhouse) -> emqx_ee_connector_clickhouse; -resource_type(dynamo) -> emqx_ee_connector_dynamo. +resource_type(dynamo) -> emqx_ee_connector_dynamo; +resource_type(rocketmq) -> emqx_ee_connector_rocketmq; +resource_type(cassandra) -> emqx_ee_connector_cassa. fields(bridges) -> [ @@ -128,6 +134,22 @@ fields(bridges) -> desc => <<"Dynamo Bridge Config">>, required => false } + )}, + {rocketmq, + mk( + hoconsc:map(name, ref(emqx_ee_bridge_rocketmq, "config")), + #{ + desc => <<"RocketMQ Bridge Config">>, + required => false + } + )}, + {cassandra, + mk( + hoconsc:map(name, ref(emqx_ee_bridge_cassa, "config")), + #{ + desc => <<"Cassandra Bridge Config">>, + required => false + } )} ] ++ kafka_structs() ++ mongodb_structs() ++ influxdb_structs() ++ redis_structs() ++ pgsql_structs() ++ clickhouse_structs(). diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_cassa.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_cassa.erl new file mode 100644 index 000000000..12f86fcf7 --- /dev/null +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_cassa.erl @@ -0,0 +1,130 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +-module(emqx_ee_bridge_cassa). + +-include_lib("typerefl/include/types.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). +-include_lib("emqx_bridge/include/emqx_bridge.hrl"). +-include_lib("emqx_resource/include/emqx_resource.hrl"). + +-import(hoconsc, [mk/2, enum/1, ref/2]). + +%% schema examples +-export([ + conn_bridge_examples/1, + values/2, + fields/2 +]). + +%% schema +-export([ + namespace/0, + roots/0, + fields/1, + desc/1 +]). + +-define(DEFAULT_CQL, << + "insert into mqtt_msg(topic, msgid, sender, qos, payload, arrived, retain) " + "values (${topic}, ${id}, ${clientid}, ${qos}, ${payload}, ${timestamp}, ${flags.retain})" +>>). + +%%-------------------------------------------------------------------- +%% schema examples + +conn_bridge_examples(Method) -> + [ + #{ + <<"cassandra">> => #{ + summary => <<"Cassandra Bridge">>, + value => values(Method, cassandra) + } + } + ]. + +%% no difference in get/post/put method +values(_Method, Type) -> + #{ + enable => true, + type => Type, + name => <<"foo">>, + servers => <<"127.0.0.1:9042">>, + keyspace => <<"mqtt">>, + pool_size => 8, + username => <<"root">>, + password => <<"******">>, + cql => ?DEFAULT_CQL, + local_topic => <<"local/topic/#">>, + resource_opts => #{ + worker_pool_size => 8, + health_check_interval => ?HEALTHCHECK_INTERVAL_RAW, + auto_restart_interval => ?AUTO_RESTART_INTERVAL_RAW, + batch_size => ?DEFAULT_BATCH_SIZE, + batch_time => ?DEFAULT_BATCH_TIME, + query_mode => sync, + max_queue_bytes => ?DEFAULT_QUEUE_SIZE + } + }. + +%%-------------------------------------------------------------------- +%% schema + +namespace() -> "bridge_cassa". + +roots() -> []. + +fields("config") -> + [ + {enable, mk(boolean(), #{desc => ?DESC("config_enable"), default => true})}, + {cql, + mk( + binary(), + #{desc => ?DESC("cql_template"), default => ?DEFAULT_CQL, format => <<"sql">>} + )}, + {local_topic, + mk( + binary(), + #{desc => ?DESC("local_topic"), default => undefined} + )}, + {resource_opts, + mk( + ref(?MODULE, "creation_opts"), + #{ + required => false, + default => #{}, + desc => ?DESC(emqx_resource_schema, <<"resource_opts">>) + } + )} + ] ++ + (emqx_ee_connector_cassa:fields(config) -- + emqx_connector_schema_lib:prepare_statement_fields()); +fields("creation_opts") -> + emqx_resource_schema:fields("creation_opts_sync_only"); +fields("post") -> + fields("post", cassandra); +fields("put") -> + fields("config"); +fields("get") -> + emqx_bridge_schema:status_fields() ++ fields("post"). + +fields("post", Type) -> + [type_field(Type), name_field() | fields("config")]. + +desc("config") -> + ?DESC("desc_config"); +desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" -> + ["Configuration for Cassandra using `", string:to_upper(Method), "` method."]; +desc("creation_opts" = Name) -> + emqx_resource_schema:desc(Name); +desc(_) -> + undefined. + +%%-------------------------------------------------------------------- +%% utils + +type_field(Type) -> + {type, mk(enum([Type]), #{required => true, desc => ?DESC("desc_type")})}. + +name_field() -> + {name, mk(binary(), #{required => true, desc => ?DESC("desc_name")})}. diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_clickhouse.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_clickhouse.erl index 1d6ecce7d..80a317d2b 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_clickhouse.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_clickhouse.erl @@ -103,8 +103,7 @@ fields("config") -> ] ++ emqx_ee_connector_clickhouse:fields(config); fields("creation_opts") -> - Opts = emqx_resource_schema:fields("creation_opts"), - [O || {Field, _} = O <- Opts, not is_hidden_opts(Field)]; + emqx_resource_schema:fields("creation_opts"); fields("post") -> fields("post", clickhouse); fields("put") -> @@ -127,10 +126,6 @@ desc(_) -> %% ------------------------------------------------------------------------------------------------- %% internal %% ------------------------------------------------------------------------------------------------- -is_hidden_opts(Field) -> - lists:member(Field, [ - async_inflight_window - ]). type_field(Type) -> {type, mk(enum([Type]), #{required => true, desc => ?DESC("desc_type")})}. diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl index 7cf7ea55e..1ad3af23c 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl @@ -3,6 +3,7 @@ %%-------------------------------------------------------------------- -module(emqx_ee_bridge_influxdb). +-include_lib("emqx/include/logger.hrl"). -include_lib("emqx_connector/include/emqx_connector.hrl"). -include_lib("typerefl/include/types.hrl"). -include_lib("hocon/include/hoconsc.hrl"). @@ -168,53 +169,150 @@ write_syntax(_) -> undefined. to_influx_lines(RawLines) -> - Lines = string:tokens(str(RawLines), "\n"), - lists:reverse(lists:foldl(fun converter_influx_line/2, [], Lines)). - -converter_influx_line(Line, AccIn) -> - case string:tokens(str(Line), " ") of - [MeasurementAndTags, Fields, Timestamp] -> - append_influx_item(MeasurementAndTags, Fields, Timestamp, AccIn); - [MeasurementAndTags, Fields] -> - append_influx_item(MeasurementAndTags, Fields, undefined, AccIn); - _ -> - throw("Bad InfluxDB Line Protocol schema") + try + influx_lines(str(RawLines), []) + catch + _:Reason:Stacktrace -> + Msg = lists:flatten( + io_lib:format("Unable to parse InfluxDB line protocol: ~p", [RawLines]) + ), + ?SLOG(error, #{msg => Msg, error_reason => Reason, stacktrace => Stacktrace}), + throw(Msg) end. -append_influx_item(MeasurementAndTags, Fields, Timestamp, Acc) -> - {Measurement, Tags} = split_measurement_and_tags(MeasurementAndTags), - [ - #{ - measurement => Measurement, - tags => kv_pairs(Tags), - fields => kv_pairs(string:tokens(Fields, ",")), - timestamp => Timestamp - } - | Acc - ]. +-define(MEASUREMENT_ESC_CHARS, [$,, $\s]). +-define(TAG_FIELD_KEY_ESC_CHARS, [$,, $=, $\s]). +-define(FIELD_VAL_ESC_CHARS, [$", $\\]). +% Common separator for both tags and fields +-define(SEP, $\s). +-define(MEASUREMENT_TAG_SEP, $,). +-define(KEY_SEP, $=). +-define(VAL_SEP, $,). +-define(NON_EMPTY, [_ | _]). -split_measurement_and_tags(Subject) -> - case string:tokens(Subject, ",") of - [] -> - throw("Bad Measurement schema"); - [Measurement] -> - {Measurement, []}; - [Measurement | Tags] -> - {Measurement, Tags} - end. +influx_lines([] = _RawLines, Acc) -> + ?NON_EMPTY = lists:reverse(Acc); +influx_lines(RawLines, Acc) -> + {Acc1, RawLines1} = influx_line(string:trim(RawLines, leading, "\s\n"), Acc), + influx_lines(RawLines1, Acc1). -kv_pairs(Pairs) -> - kv_pairs(Pairs, []). -kv_pairs([], Acc) -> - lists:reverse(Acc); -kv_pairs([Pair | Rest], Acc) -> - case string:tokens(Pair, "=") of - [K, V] -> - %% Reduplicated keys will be overwritten. Follows InfluxDB Line Protocol. - kv_pairs(Rest, [{K, V} | Acc]); - _ -> - throw(io_lib:format("Bad InfluxDB Line Protocol Key Value pair: ~p", Pair)) - end. +influx_line([], Acc) -> + {Acc, []}; +influx_line(Line, Acc) -> + {?NON_EMPTY = Measurement, Line1} = measurement(Line), + {Tags, Line2} = tags(Line1), + {?NON_EMPTY = Fields, Line3} = influx_fields(Line2), + {Timestamp, Line4} = timestamp(Line3), + { + [ + #{ + measurement => Measurement, + tags => Tags, + fields => Fields, + timestamp => Timestamp + } + | Acc + ], + Line4 + }. + +measurement(Line) -> + unescape(?MEASUREMENT_ESC_CHARS, [?MEASUREMENT_TAG_SEP, ?SEP], Line, []). + +tags([?MEASUREMENT_TAG_SEP | Line]) -> + tags1(Line, []); +tags(Line) -> + {[], Line}. + +%% Empty line is invalid as fields are required after tags, +%% need to break recursion here and fail later on parsing fields +tags1([] = Line, Acc) -> + {lists:reverse(Acc), Line}; +%% Matching non empty Acc treats lines like "m, field=field_val" invalid +tags1([?SEP | _] = Line, ?NON_EMPTY = Acc) -> + {lists:reverse(Acc), Line}; +tags1(Line, Acc) -> + {Tag, Line1} = tag(Line), + tags1(Line1, [Tag | Acc]). + +tag(Line) -> + {?NON_EMPTY = Key, Line1} = key(Line), + {?NON_EMPTY = Val, Line2} = tag_val(Line1), + {{Key, Val}, Line2}. + +tag_val(Line) -> + {Val, Line1} = unescape(?TAG_FIELD_KEY_ESC_CHARS, [?VAL_SEP, ?SEP], Line, []), + {Val, strip_l(Line1, ?VAL_SEP)}. + +influx_fields([?SEP | Line]) -> + fields1(string:trim(Line, leading, "\s"), []). + +%% Timestamp is optional, so fields may be at the very end of the line +fields1([Ch | _] = Line, Acc) when Ch =:= ?SEP; Ch =:= $\n -> + {lists:reverse(Acc), Line}; +fields1([] = Line, Acc) -> + {lists:reverse(Acc), Line}; +fields1(Line, Acc) -> + {Field, Line1} = field(Line), + fields1(Line1, [Field | Acc]). + +field(Line) -> + {?NON_EMPTY = Key, Line1} = key(Line), + {Val, Line2} = field_val(Line1), + {{Key, Val}, Line2}. + +field_val([$" | Line]) -> + {Val, [$" | Line1]} = unescape(?FIELD_VAL_ESC_CHARS, [$"], Line, []), + %% Quoted val can be empty + {Val, strip_l(Line1, ?VAL_SEP)}; +field_val(Line) -> + %% Unquoted value should not be un-escaped according to InfluxDB protocol, + %% as it can only hold float, integer, uinteger or boolean value. + %% However, as templates are possible, un-escaping is applied here, + %% which also helps to detect some invalid lines, e.g.: "m,tag=1 field= ${timestamp}" + {Val, Line1} = unescape(?TAG_FIELD_KEY_ESC_CHARS, [?VAL_SEP, ?SEP, $\n], Line, []), + {?NON_EMPTY = Val, strip_l(Line1, ?VAL_SEP)}. + +timestamp([?SEP | Line]) -> + Line1 = string:trim(Line, leading, "\s"), + %% Similarly to unquoted field value, un-escape a timestamp to validate and handle + %% potentially escaped characters in a template + {T, Line2} = unescape(?TAG_FIELD_KEY_ESC_CHARS, [?SEP, $\n], Line1, []), + {timestamp1(T), Line2}; +timestamp(Line) -> + {undefined, Line}. + +timestamp1(?NON_EMPTY = Ts) -> Ts; +timestamp1(_Ts) -> undefined. + +%% Common for both tag and field keys +key(Line) -> + {Key, Line1} = unescape(?TAG_FIELD_KEY_ESC_CHARS, [?KEY_SEP], Line, []), + {Key, strip_l(Line1, ?KEY_SEP)}. + +%% Only strip a character between pairs, don't strip it(and let it fail) +%% if the char to be stripped is at the end, e.g.: m,tag=val, field=val +strip_l([Ch, Ch1 | Str], Ch) when Ch1 =/= ?SEP -> + [Ch1 | Str]; +strip_l(Str, _Ch) -> + Str. + +unescape(EscapeChars, SepChars, [$\\, Char | T], Acc) -> + ShouldEscapeBackslash = lists:member($\\, EscapeChars), + Acc1 = + case lists:member(Char, EscapeChars) of + true -> [Char | Acc]; + false when not ShouldEscapeBackslash -> [Char, $\\ | Acc] + end, + unescape(EscapeChars, SepChars, T, Acc1); +unescape(EscapeChars, SepChars, [Char | T] = L, Acc) -> + IsEscapeChar = lists:member(Char, EscapeChars), + case lists:member(Char, SepChars) of + true -> {lists:reverse(Acc), L}; + false when not IsEscapeChar -> unescape(EscapeChars, SepChars, T, [Char | Acc]) + end; +unescape(_EscapeChars, _SepChars, [] = L, Acc) -> + {lists:reverse(Acc), L}. str(A) when is_atom(A) -> atom_to_list(A); diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl index 4a9263134..529e28dea 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl @@ -233,7 +233,7 @@ fields(socket_opts) -> boolean(), #{ default => true, - hidden => true, + importance => ?IMPORTANCE_HIDDEN, desc => ?DESC(socket_nodelay) } )} @@ -368,7 +368,7 @@ fields(consumer_kafka_opts) -> })}, {max_rejoin_attempts, mk(non_neg_integer(), #{ - hidden => true, + importance => ?IMPORTANCE_HIDDEN, default => 5, desc => ?DESC(consumer_max_rejoin_attempts) })}, diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_rocketmq.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_rocketmq.erl new file mode 100644 index 000000000..124e18069 --- /dev/null +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_rocketmq.erl @@ -0,0 +1,120 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +-module(emqx_ee_bridge_rocketmq). + +-include_lib("typerefl/include/types.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). +-include_lib("emqx_bridge/include/emqx_bridge.hrl"). +-include_lib("emqx_resource/include/emqx_resource.hrl"). + +-import(hoconsc, [mk/2, enum/1, ref/2]). + +-export([ + conn_bridge_examples/1, + values/1 +]). + +-export([ + namespace/0, + roots/0, + fields/1, + desc/1 +]). + +-define(DEFAULT_TEMPLATE, <<>>). +-define(DEFFAULT_REQ_TIMEOUT, <<"15s">>). + +%% ------------------------------------------------------------------------------------------------- +%% api + +conn_bridge_examples(Method) -> + [ + #{ + <<"rocketmq">> => #{ + summary => <<"RocketMQ Bridge">>, + value => values(Method) + } + } + ]. + +values(get) -> + values(post); +values(post) -> + #{ + enable => true, + type => rocketmq, + name => <<"foo">>, + server => <<"127.0.0.1:9876">>, + topic => <<"TopicTest">>, + template => ?DEFAULT_TEMPLATE, + local_topic => <<"local/topic/#">>, + resource_opts => #{ + worker_pool_size => 1, + health_check_interval => ?HEALTHCHECK_INTERVAL_RAW, + auto_restart_interval => ?AUTO_RESTART_INTERVAL_RAW, + batch_size => ?DEFAULT_BATCH_SIZE, + batch_time => ?DEFAULT_BATCH_TIME, + query_mode => sync, + max_queue_bytes => ?DEFAULT_QUEUE_SIZE + } + }; +values(put) -> + values(post). + +%% ------------------------------------------------------------------------------------------------- +%% Hocon Schema Definitions +namespace() -> "bridge_rocketmq". + +roots() -> []. + +fields("config") -> + [ + {enable, mk(boolean(), #{desc => ?DESC("config_enable"), default => true})}, + {template, + mk( + binary(), + #{desc => ?DESC("template"), default => ?DEFAULT_TEMPLATE} + )}, + {local_topic, + mk( + binary(), + #{desc => ?DESC("local_topic"), required => false} + )}, + {resource_opts, + mk( + ref(?MODULE, "creation_opts"), + #{ + required => false, + default => #{<<"request_timeout">> => ?DEFFAULT_REQ_TIMEOUT}, + desc => ?DESC(emqx_resource_schema, <<"resource_opts">>) + } + )} + ] ++ + (emqx_ee_connector_rocketmq:fields(config) -- + emqx_connector_schema_lib:prepare_statement_fields()); +fields("creation_opts") -> + emqx_resource_schema:fields("creation_opts_sync_only"); +fields("post") -> + [type_field(), name_field() | fields("config")]; +fields("put") -> + fields("config"); +fields("get") -> + emqx_bridge_schema:status_fields() ++ fields("post"). + +desc("config") -> + ?DESC("desc_config"); +desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" -> + ["Configuration for RocketMQ using `", string:to_upper(Method), "` method."]; +desc("creation_opts" = Name) -> + emqx_resource_schema:desc(Name); +desc(_) -> + undefined. + +%% ------------------------------------------------------------------------------------------------- + +type_field() -> + {type, mk(enum([rocketmq]), #{required => true, desc => ?DESC("desc_type")})}. + +name_field() -> + {name, mk(binary(), #{required => true, desc => ?DESC("desc_name")})}. diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_tdengine.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_tdengine.erl index b72d79955..f031cbfbf 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_tdengine.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_tdengine.erl @@ -22,7 +22,7 @@ ]). -define(DEFAULT_SQL, << - "insert into mqtt.t_mqtt_msg(ts, msgid, mqtt_topic, qos, payload, arrived) " + "insert into t_mqtt_msg(ts, msgid, mqtt_topic, qos, payload, arrived) " "values (${ts}, ${id}, ${topic}, ${qos}, ${payload}, ${timestamp})" >>). diff --git a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_consumer_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_consumer_SUITE.erl index 01f62e04c..02a4c3c3b 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_consumer_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_consumer_SUITE.erl @@ -1635,7 +1635,11 @@ t_bridge_rule_action_source(Config) -> }, emqx_json:decode(RawPayload, [return_maps]) ), - ?assertEqual(1, emqx_resource_metrics:received_get(ResourceId)), + ?retry( + _Interval = 200, + _NAttempts = 20, + ?assertEqual(1, emqx_resource_metrics:received_get(ResourceId)) + ), ok end ), diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_cassa_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_cassa_SUITE.erl new file mode 100644 index 000000000..d040000e2 --- /dev/null +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_cassa_SUITE.erl @@ -0,0 +1,603 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_ee_bridge_cassa_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +% SQL definitions +-define(SQL_BRIDGE, + "insert into mqtt_msg_test(topic, payload, arrived) " + "values (${topic}, ${payload}, ${timestamp})" +). +-define(SQL_CREATE_TABLE, + "" + "\n" + "CREATE TABLE mqtt.mqtt_msg_test (\n" + " topic text,\n" + " payload text,\n" + " arrived timestamp,\n" + " PRIMARY KEY (topic)\n" + ");\n" + "" +). +-define(SQL_DROP_TABLE, "DROP TABLE mqtt.mqtt_msg_test"). +-define(SQL_DELETE, "TRUNCATE mqtt.mqtt_msg_test"). +-define(SQL_SELECT, "SELECT payload FROM mqtt.mqtt_msg_test"). + +% DB defaults +-define(CASSA_KEYSPACE, "mqtt"). +-define(CASSA_USERNAME, "cassandra"). +-define(CASSA_PASSWORD, "cassandra"). +-define(BATCH_SIZE, 10). + +%% cert files for client +-define(CERT_ROOT, + filename:join([emqx_common_test_helpers:proj_root(), ".ci", "docker-compose-file", "certs"]) +). + +-define(CAFILE, filename:join(?CERT_ROOT, ["ca.crt"])). +-define(CERTFILE, filename:join(?CERT_ROOT, ["client.pem"])). +-define(KEYFILE, filename:join(?CERT_ROOT, ["client.key"])). + +%% How to run it locally: +%% 1. Start all deps services +%% sudo docker compose -f .ci/docker-compose-file/docker-compose.yaml \ +%% -f .ci/docker-compose-file/docker-compose-cassandra.yaml \ +%% -f .ci/docker-compose-file/docker-compose-toxiproxy.yaml \ +%% up --build +%% +%% 2. Run use cases with special environment variables +%% CASSA_TCP_HOST=127.0.0.1 CASSA_TCP_PORT=19042 \ +%% CASSA_TLS_HOST=127.0.0.1 CASSA_TLS_PORT=19142 \ +%% PROXY_HOST=127.0.0.1 ./rebar3 as test ct -c -v --name ct@127.0.0.1 \ +%% --suite lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_cassa_SUITE.erl +%% + +%%------------------------------------------------------------------------------ +%% CT boilerplate +%%------------------------------------------------------------------------------ + +all() -> + [ + {group, tcp}, + {group, tls} + ]. + +groups() -> + TCs = emqx_common_test_helpers:all(?MODULE), + NonBatchCases = [t_write_timeout], + [ + {tcp, [ + %{group, with_batch}, + {group, without_batch} + ]}, + {tls, [ + %{group, with_batch}, + {group, without_batch} + ]}, + {with_batch, TCs -- NonBatchCases}, + {without_batch, TCs} + ]. + +init_per_group(tcp, Config) -> + Host = os:getenv("CASSA_TCP_HOST", "toxiproxy"), + Port = list_to_integer(os:getenv("CASSA_TCP_PORT", "9042")), + [ + {cassa_host, Host}, + {cassa_port, Port}, + {enable_tls, false}, + {query_mode, sync}, + {proxy_name, "cassa_tcp"} + | Config + ]; +init_per_group(tls, Config) -> + Host = os:getenv("CASSA_TLS_HOST", "toxiproxy"), + Port = list_to_integer(os:getenv("CASSA_TLS_PORT", "9142")), + [ + {cassa_host, Host}, + {cassa_port, Port}, + {enable_tls, true}, + {query_mode, sync}, + {proxy_name, "cassa_tls"} + | Config + ]; +init_per_group(with_batch, Config0) -> + Config = [{enable_batch, true} | Config0], + common_init(Config); +init_per_group(without_batch, Config0) -> + Config = [{enable_batch, false} | Config0], + common_init(Config); +init_per_group(_Group, Config) -> + Config. + +end_per_group(Group, Config) when + Group == without_batch; Group == without_batch +-> + connect_and_drop_table(Config), + ProxyHost = ?config(proxy_host, Config), + ProxyPort = ?config(proxy_port, Config), + emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), + ok; +end_per_group(_Group, _Config) -> + ok. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + emqx_mgmt_api_test_util:end_suite(), + ok = emqx_common_test_helpers:stop_apps([emqx_bridge, emqx_conf]), + ok. + +init_per_testcase(_Testcase, Config) -> + connect_and_clear_table(Config), + delete_bridge(Config), + Config. + +end_per_testcase(_Testcase, Config) -> + ProxyHost = ?config(proxy_host, Config), + ProxyPort = ?config(proxy_port, Config), + emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), + connect_and_clear_table(Config), + ok = snabbkaffe:stop(), + delete_bridge(Config), + ok. + +%%------------------------------------------------------------------------------ +%% Helper fns +%%------------------------------------------------------------------------------ + +common_init(Config0) -> + ct:pal("commit_init: ~p~n", [Config0]), + BridgeType = proplists:get_value(bridge_type, Config0, <<"cassandra">>), + Host = ?config(cassa_host, Config0), + Port = ?config(cassa_port, Config0), + case emqx_common_test_helpers:is_tcp_server_available(Host, Port) of + true -> + % Setup toxiproxy + ProxyHost = os:getenv("PROXY_HOST", "toxiproxy"), + ProxyPort = list_to_integer(os:getenv("PROXY_PORT", "8474")), + emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), + % Ensure EE bridge module is loaded + _ = application:load(emqx_ee_bridge), + _ = emqx_ee_bridge:module_info(), + ok = emqx_common_test_helpers:start_apps([emqx_conf, emqx_bridge]), + emqx_mgmt_api_test_util:init_suite(), + % Connect to cassnadra directly and create the table + connect_and_create_table(Config0), + {Name, CassaConf} = cassa_config(BridgeType, Config0), + Config = + [ + {cassa_config, CassaConf}, + {cassa_bridge_type, BridgeType}, + {cassa_name, Name}, + {proxy_host, ProxyHost}, + {proxy_port, ProxyPort} + | Config0 + ], + Config; + false -> + case os:getenv("IS_CI") of + "yes" -> + throw(no_cassandra); + _ -> + {skip, no_cassandra} + end + end. + +cassa_config(BridgeType, Config) -> + Port = integer_to_list(?config(cassa_port, Config)), + Server = ?config(cassa_host, Config) ++ ":" ++ Port, + Name = atom_to_binary(?MODULE), + BatchSize = + case ?config(enable_batch, Config) of + true -> ?BATCH_SIZE; + false -> 1 + end, + QueryMode = ?config(query_mode, Config), + TlsEnabled = ?config(enable_tls, Config), + ConfigString = + io_lib:format( + "bridges.~s.~s {\n" + " enable = true\n" + " servers = ~p\n" + " keyspace = ~p\n" + " username = ~p\n" + " password = ~p\n" + " cql = ~p\n" + " resource_opts = {\n" + " request_timeout = 500ms\n" + " batch_size = ~b\n" + " query_mode = ~s\n" + " }\n" + " ssl = {\n" + " enable = ~w\n" + " cacertfile = \"~s\"\n" + " certfile = \"~s\"\n" + " keyfile = \"~s\"\n" + " server_name_indication = disable\n" + " }\n" + "}", + [ + BridgeType, + Name, + Server, + ?CASSA_KEYSPACE, + ?CASSA_USERNAME, + ?CASSA_PASSWORD, + ?SQL_BRIDGE, + BatchSize, + QueryMode, + TlsEnabled, + ?CAFILE, + ?CERTFILE, + ?KEYFILE + ] + ), + {Name, parse_and_check(ConfigString, BridgeType, Name)}. + +parse_and_check(ConfigString, BridgeType, Name) -> + {ok, RawConf} = hocon:binary(ConfigString, #{format => map}), + hocon_tconf:check_plain(emqx_bridge_schema, RawConf, #{required => false, atom_key => false}), + #{<<"bridges">> := #{BridgeType := #{Name := Config}}} = RawConf, + Config. + +create_bridge(Config) -> + BridgeType = ?config(cassa_bridge_type, Config), + Name = ?config(cassa_name, Config), + BridgeConfig = ?config(cassa_config, Config), + emqx_bridge:create(BridgeType, Name, BridgeConfig). + +delete_bridge(Config) -> + BridgeType = ?config(cassa_bridge_type, Config), + Name = ?config(cassa_name, Config), + emqx_bridge:remove(BridgeType, Name). + +create_bridge_http(Params) -> + Path = emqx_mgmt_api_test_util:api_path(["bridges"]), + AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + case emqx_mgmt_api_test_util:request_api(post, Path, "", AuthHeader, Params) of + {ok, Res} -> {ok, emqx_json:decode(Res, [return_maps])}; + Error -> Error + end. + +bridges_probe_http(Params) -> + Path = emqx_mgmt_api_test_util:api_path(["bridges_probe"]), + AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + case emqx_mgmt_api_test_util:request_api(post, Path, "", AuthHeader, Params) of + {ok, _} -> ok; + Error -> Error + end. + +send_message(Config, Payload) -> + Name = ?config(cassa_name, Config), + BridgeType = ?config(cassa_bridge_type, Config), + BridgeID = emqx_bridge_resource:bridge_id(BridgeType, Name), + emqx_bridge:send_message(BridgeID, Payload). + +query_resource(Config, Request) -> + Name = ?config(cassa_name, Config), + BridgeType = ?config(cassa_bridge_type, Config), + ResourceID = emqx_bridge_resource:resource_id(BridgeType, Name), + emqx_resource:query(ResourceID, Request, #{timeout => 1_000}). + +connect_direct_cassa(Config) -> + Opts = #{ + nodes => [{?config(cassa_host, Config), ?config(cassa_port, Config)}], + username => ?CASSA_USERNAME, + password => ?CASSA_PASSWORD, + keyspace => ?CASSA_KEYSPACE + }, + SslOpts = + case ?config(enable_tls, Config) of + true -> + Opts#{ + ssl => emqx_tls_lib:to_client_opts( + #{ + enable => true, + cacertfile => ?CAFILE, + certfile => ?CERTFILE, + keyfile => ?KEYFILE + } + ) + }; + false -> + Opts + end, + {ok, Con} = ecql:connect(maps:to_list(SslOpts)), + Con. + +% These funs connect and then stop the cassandra connection +connect_and_create_table(Config) -> + with_direct_conn(Config, fun(Conn) -> + {ok, _} = ecql:query(Conn, ?SQL_CREATE_TABLE) + end). + +connect_and_drop_table(Config) -> + with_direct_conn(Config, fun(Conn) -> + {ok, _} = ecql:query(Conn, ?SQL_DROP_TABLE) + end). + +connect_and_clear_table(Config) -> + with_direct_conn(Config, fun(Conn) -> + ok = ecql:query(Conn, ?SQL_DELETE) + end). + +connect_and_get_payload(Config) -> + with_direct_conn(Config, fun(Conn) -> + {ok, {_Keyspace, _ColsSpec, [[Result]]}} = ecql:query(Conn, ?SQL_SELECT), + Result + end). + +with_direct_conn(Config, Fn) -> + Conn = connect_direct_cassa(Config), + try + Fn(Conn) + after + ok = ecql:close(Conn) + end. + +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ + +t_setup_via_config_and_publish(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + Val = integer_to_binary(erlang:unique_integer()), + SentData = #{ + topic => atom_to_binary(?FUNCTION_NAME), + payload => Val, + timestamp => 1668602148000 + }, + ?check_trace( + begin + ?wait_async_action( + ?assertEqual(ok, send_message(Config, SentData)), + #{?snk_kind := cassandra_connector_query_return}, + 10_000 + ), + ?assertMatch( + Val, + connect_and_get_payload(Config) + ), + ok + end, + fun(Trace0) -> + Trace = ?of_kind(cassandra_connector_query_return, Trace0), + case ?config(enable_batch, Config) of + true -> + ?assertMatch([#{result := {_, [ok]}}], Trace); + false -> + ?assertMatch([#{result := ok}], Trace) + end, + ok + end + ), + ok. + +t_setup_via_http_api_and_publish(Config) -> + BridgeType = ?config(cassa_bridge_type, Config), + Name = ?config(cassa_name, Config), + BridgeConfig0 = ?config(cassa_config, Config), + BridgeConfig = BridgeConfig0#{ + <<"name">> => Name, + <<"type">> => BridgeType + }, + ?assertMatch( + {ok, _}, + create_bridge_http(BridgeConfig) + ), + Val = integer_to_binary(erlang:unique_integer()), + SentData = #{ + topic => atom_to_binary(?FUNCTION_NAME), + payload => Val, + timestamp => 1668602148000 + }, + ?check_trace( + begin + ?wait_async_action( + ?assertEqual(ok, send_message(Config, SentData)), + #{?snk_kind := cassandra_connector_query_return}, + 10_000 + ), + ?assertMatch( + Val, + connect_and_get_payload(Config) + ), + ok + end, + fun(Trace0) -> + Trace = ?of_kind(cassandra_connector_query_return, Trace0), + case ?config(enable_batch, Config) of + true -> + ?assertMatch([#{result := {_, [{ok, 1}]}}], Trace); + false -> + ?assertMatch([#{result := ok}], Trace) + end, + ok + end + ), + ok. + +t_get_status(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + ProxyPort = ?config(proxy_port, Config), + ProxyHost = ?config(proxy_host, Config), + ProxyName = ?config(proxy_name, Config), + + Name = ?config(cassa_name, Config), + BridgeType = ?config(cassa_bridge_type, Config), + ResourceID = emqx_bridge_resource:resource_id(BridgeType, Name), + + ?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceID)), + emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> + ?assertMatch( + {ok, Status} when Status =:= disconnected orelse Status =:= connecting, + emqx_resource_manager:health_check(ResourceID) + ) + end), + ok. + +t_bridges_probe_via_http(Config) -> + BridgeType = ?config(cassa_bridge_type, Config), + Name = ?config(cassa_name, Config), + BridgeConfig0 = ?config(cassa_config, Config), + BridgeConfig = BridgeConfig0#{ + <<"name">> => Name, + <<"type">> => BridgeType + }, + ?assertMatch(ok, bridges_probe_http(BridgeConfig)), + + ok. + +t_create_disconnected(Config) -> + ProxyPort = ?config(proxy_port, Config), + ProxyHost = ?config(proxy_host, Config), + ProxyName = ?config(proxy_name, Config), + ?check_trace( + emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> + ?assertMatch({ok, _}, create_bridge(Config)) + end), + fun(Trace) -> + ?assertMatch( + [#{error := {start_pool_failed, _, _}}], + ?of_kind(cassandra_connector_start_failed, Trace) + ), + ok + end + ), + ok. + +t_write_failure(Config) -> + ProxyName = ?config(proxy_name, Config), + ProxyPort = ?config(proxy_port, Config), + ProxyHost = ?config(proxy_host, Config), + QueryMode = ?config(query_mode, Config), + {ok, _} = create_bridge(Config), + Val = integer_to_binary(erlang:unique_integer()), + SentData = #{ + topic => atom_to_binary(?FUNCTION_NAME), + payload => Val, + timestamp => 1668602148000 + }, + ?check_trace( + emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> + {_, {ok, _}} = + ?wait_async_action( + case QueryMode of + sync -> + ?assertMatch({error, _}, send_message(Config, SentData)); + async -> + send_message(Config, SentData) + end, + #{?snk_kind := buffer_worker_flush_nack}, + 1_000 + ) + end), + fun(Trace0) -> + ct:pal("trace: ~p", [Trace0]), + Trace = ?of_kind(buffer_worker_flush_nack, Trace0), + ?assertMatch([#{result := {error, _}} | _], Trace), + [#{result := {error, Error}} | _] = Trace, + case Error of + {resource_error, _} -> + ok; + {recoverable_error, disconnected} -> + ok; + _ -> + ct:fail("unexpected error: ~p", [Error]) + end + end + ), + ok. + +%% This test doesn't work with batch enabled since it is not possible +%% to set the timeout directly for batch queries +%% +%% XXX: parameter with request timeout is not supported yet. +%% +%t_write_timeout(Config) -> +% ProxyName = ?config(proxy_name, Config), +% ProxyPort = ?config(proxy_port, Config), +% ProxyHost = ?config(proxy_host, Config), +% {ok, _} = create_bridge(Config), +% Val = integer_to_binary(erlang:unique_integer()), +% SentData = #{payload => Val, timestamp => 1668602148000}, +% Timeout = 1000, +% emqx_common_test_helpers:with_failure(timeout, ProxyName, ProxyHost, ProxyPort, fun() -> +% ?assertMatch( +% {error, {resource_error, #{reason := timeout}}}, +% query_resource(Config, {send_message, SentData, [], Timeout}) +% ) +% end), +% ok. + +t_simple_sql_query(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + Request = {query, <<"SELECT count(1) AS T FROM system.local">>}, + Result = query_resource(Config, Request), + case ?config(enable_batch, Config) of + true -> ?assertEqual({error, {unrecoverable_error, batch_prepare_not_implemented}}, Result); + false -> ?assertMatch({ok, {<<"system.local">>, _, [[1]]}}, Result) + end, + ok. + +t_missing_data(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + %% emqx_ee_connector_cassa will send missed data as a `null` atom + %% to ecql driver + Result = send_message(Config, #{}), + ?assertMatch( + %% TODO: match error msgs + {error, {unrecoverable_error, {8704, <<"Expected 8 or 0 byte long for date (4)">>}}}, + Result + ), + ok. + +t_bad_sql_parameter(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + Request = {query, <<"">>, [bad_parameter]}, + Result = query_resource(Config, Request), + case ?config(enable_batch, Config) of + true -> + ?assertEqual({error, {unrecoverable_error, invalid_request}}, Result); + false -> + ?assertMatch( + {error, {unrecoverable_error, _}}, Result + ) + end, + ok. + +t_nasty_sql_string(Config) -> + ?assertMatch({ok, _}, create_bridge(Config)), + Payload = list_to_binary(lists:seq(1, 127)), + Message = #{ + topic => atom_to_binary(?FUNCTION_NAME), + payload => Payload, + timestamp => erlang:system_time(millisecond) + }, + %% XXX: why ok instead of {ok, AffectedLines}? + ?assertEqual(ok, send_message(Config, Message)), + ?assertEqual(Payload, connect_and_get_payload(Config)). diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_dynamo_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_dynamo_SUITE.erl index 26666c6d8..c0d58c4f7 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_dynamo_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_dynamo_SUITE.erl @@ -83,9 +83,10 @@ end_per_suite(_Config) -> ok = emqx_common_test_helpers:stop_apps([emqx_bridge, emqx_conf]), ok. -init_per_testcase(_Testcase, Config) -> +init_per_testcase(TestCase, Config) -> create_table(Config), - Config. + ok = snabbkaffe:start_trace(), + [{dynamo_name, atom_to_binary(TestCase)} | Config]. end_per_testcase(_Testcase, Config) -> ProxyHost = ?config(proxy_host, Config), @@ -93,7 +94,7 @@ end_per_testcase(_Testcase, Config) -> emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), ok = snabbkaffe:stop(), delete_table(Config), - delete_bridge(Config), + delete_all_bridges(), ok. %%------------------------------------------------------------------------------ @@ -186,15 +187,22 @@ parse_and_check(ConfigString, BridgeType, Name) -> Config. create_bridge(Config) -> - BridgeType = ?config(dynamo_bridge_type, Config), - Name = ?config(dynamo_name, Config), - TDConfig = ?config(dynamo_config, Config), - emqx_bridge:create(BridgeType, Name, TDConfig). + create_bridge(Config, _Overrides = #{}). -delete_bridge(Config) -> +create_bridge(Config, Overrides) -> BridgeType = ?config(dynamo_bridge_type, Config), Name = ?config(dynamo_name, Config), - emqx_bridge:remove(BridgeType, Name). + DynamoConfig0 = ?config(dynamo_config, Config), + DynamoConfig = emqx_map_lib:deep_merge(DynamoConfig0, Overrides), + emqx_bridge:create(BridgeType, Name, DynamoConfig). + +delete_all_bridges() -> + lists:foreach( + fun(#{name := Name, type := Type}) -> + emqx_bridge:remove(Type, Name) + end, + emqx_bridge:list() + ). create_bridge_http(Params) -> Path = emqx_mgmt_api_test_util:api_path(["bridges"]), @@ -283,7 +291,7 @@ t_setup_via_config_and_publish(Config) -> end, fun(Trace0) -> Trace = ?of_kind(dynamo_connector_query_return, Trace0), - ?assertMatch([#{result := {ok, _}}], Trace), + ?assertMatch([#{result := ok}], Trace), ok end ), @@ -320,17 +328,19 @@ t_setup_via_http_api_and_publish(Config) -> end, fun(Trace0) -> Trace = ?of_kind(dynamo_connector_query_return, Trace0), - ?assertMatch([#{result := {ok, _}}], Trace), + ?assertMatch([#{result := ok}], Trace), ok end ), ok. t_get_status(Config) -> - ?assertMatch( - {ok, _}, - create_bridge(Config) - ), + {{ok, _}, {ok, _}} = + ?wait_async_action( + create_bridge(Config), + #{?snk_kind := resource_connected_enter}, + 20_000 + ), ProxyPort = ?config(proxy_port, Config), ProxyHost = ?config(proxy_host, Config), @@ -359,7 +369,12 @@ t_write_failure(Config) -> ProxyName = ?config(proxy_name, Config), ProxyPort = ?config(proxy_port, Config), ProxyHost = ?config(proxy_host, Config), - {ok, _} = create_bridge(Config), + {{ok, _}, {ok, _}} = + ?wait_async_action( + create_bridge(Config), + #{?snk_kind := resource_connected_enter}, + 20_000 + ), SentData = #{id => emqx_misc:gen_id(), payload => ?PAYLOAD}, emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> ?assertMatch( @@ -372,7 +387,12 @@ t_write_timeout(Config) -> ProxyName = ?config(proxy_name, Config), ProxyPort = ?config(proxy_port, Config), ProxyHost = ?config(proxy_host, Config), - {ok, _} = create_bridge(Config), + {{ok, _}, {ok, _}} = + ?wait_async_action( + create_bridge(Config), + #{?snk_kind := resource_connected_enter}, + 20_000 + ), SentData = #{id => emqx_misc:gen_id(), payload => ?PAYLOAD}, emqx_common_test_helpers:with_failure(timeout, ProxyName, ProxyHost, ProxyPort, fun() -> ?assertMatch( diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index 8424ddff0..75d2d2d8c 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -520,6 +520,7 @@ wait_until_gauge_is(GaugeName, ExpectedValue, Timeout) -> #{measurements := #{gauge_set := ExpectedValue}} -> ok; #{measurements := #{gauge_set := Value}} -> + ct:pal("events: ~p", [Events]), ct:fail( "gauge ~p didn't reach expected value ~p; last value: ~p", [GaugeName, ExpectedValue, Value] @@ -972,7 +973,13 @@ t_publish_econnrefused(Config) -> ResourceId = ?config(resource_id, Config), %% set pipelining to 1 so that one of the 2 requests is `pending' %% in ehttpc. - {ok, _} = create_bridge(Config, #{<<"pipelining">> => 1}), + {ok, _} = create_bridge( + Config, + #{ + <<"pipelining">> => 1, + <<"resource_opts">> => #{<<"resume_interval">> => <<"15s">>} + } + ), {ok, #{<<"id">> := RuleId}} = create_rule_and_action_http(Config), on_exit(fun() -> ok = emqx_rule_engine:delete_rule(RuleId) end), assert_empty_metrics(ResourceId), @@ -986,7 +993,10 @@ t_publish_timeout(Config) -> %% requests are done separately. {ok, _} = create_bridge(Config, #{ <<"pipelining">> => 1, - <<"resource_opts">> => #{<<"batch_size">> => 1} + <<"resource_opts">> => #{ + <<"batch_size">> => 1, + <<"resume_interval">> => <<"15s">> + } }), {ok, #{<<"id">> := RuleId}} = create_rule_and_action_http(Config), on_exit(fun() -> ok = emqx_rule_engine:delete_rule(RuleId) end), @@ -1013,7 +1023,6 @@ t_publish_timeout(Config) -> do_econnrefused_or_timeout_test(Config, timeout). do_econnrefused_or_timeout_test(Config, Error) -> - QueryMode = ?config(query_mode, Config), ResourceId = ?config(resource_id, Config), TelemetryTable = ?config(telemetry_table, Config), Topic = <<"t/topic">>, @@ -1021,15 +1030,8 @@ do_econnrefused_or_timeout_test(Config, Error) -> Message = emqx_message:make(Topic, Payload), ?check_trace( begin - case {QueryMode, Error} of - {sync, _} -> - {_, {ok, _}} = - ?wait_async_action( - emqx:publish(Message), - #{?snk_kind := gcp_pubsub_request_failed, recoverable_error := true}, - 15_000 - ); - {async, econnrefused} -> + case Error of + econnrefused -> %% at the time of writing, async requests %% are never considered expired by ehttpc %% (even if they arrive late, or never @@ -1049,7 +1051,7 @@ do_econnrefused_or_timeout_test(Config, Error) -> }, 15_000 ); - {async, timeout} -> + timeout -> %% at the time of writing, async requests %% are never considered expired by ehttpc %% (even if they arrive late, or never @@ -1067,18 +1069,13 @@ do_econnrefused_or_timeout_test(Config, Error) -> end end, fun(Trace) -> - case {QueryMode, Error} of - {sync, _} -> + case Error of + econnrefused -> ?assertMatch( [#{reason := Error, connector := ResourceId} | _], ?of_kind(gcp_pubsub_request_failed, Trace) ); - {async, econnrefused} -> - ?assertMatch( - [#{reason := Error, connector := ResourceId} | _], - ?of_kind(gcp_pubsub_request_failed, Trace) - ); - {async, timeout} -> + timeout -> ?assertMatch( [_, _ | _], ?of_kind(gcp_pubsub_response, Trace) @@ -1088,11 +1085,11 @@ do_econnrefused_or_timeout_test(Config, Error) -> end ), - case {Error, QueryMode} of + case Error of %% apparently, async with disabled queue doesn't mark the %% message as dropped; and since it never considers the %% response expired, this succeeds. - {econnrefused, async} -> + econnrefused -> wait_telemetry_event(TelemetryTable, queuing, ResourceId, #{ timeout => 10_000, n_events => 1 }), @@ -1114,7 +1111,7 @@ do_econnrefused_or_timeout_test(Config, Error) -> } when Matched >= 1 andalso Inflight + Queueing + Dropped + Failed =< 2, CurrentMetrics ); - {timeout, async} -> + timeout -> wait_until_gauge_is(inflight, 0, _Timeout = 400), wait_until_gauge_is(queuing, 0, _Timeout = 400), assert_metrics( @@ -1129,21 +1126,6 @@ do_econnrefused_or_timeout_test(Config, Error) -> late_reply => 2 }, ResourceId - ); - {_, sync} -> - wait_until_gauge_is(queuing, 0, 500), - wait_until_gauge_is(inflight, 1, 500), - assert_metrics( - #{ - dropped => 0, - failed => 0, - inflight => 1, - matched => 1, - queuing => 0, - retried => 0, - success => 0 - }, - ResourceId ) end, @@ -1267,7 +1249,6 @@ t_failure_no_body(Config) -> t_unrecoverable_error(Config) -> ResourceId = ?config(resource_id, Config), - QueryMode = ?config(query_mode, Config), TestPid = self(), FailureNoBodyHandler = fun(Req0, State) -> @@ -1298,33 +1279,16 @@ t_unrecoverable_error(Config) -> Message = emqx_message:make(Topic, Payload), ?check_trace( {_, {ok, _}} = - case QueryMode of - sync -> - ?wait_async_action( - emqx:publish(Message), - #{?snk_kind := gcp_pubsub_request_failed}, - 5_000 - ); - async -> - ?wait_async_action( - emqx:publish(Message), - #{?snk_kind := gcp_pubsub_response}, - 5_000 - ) - end, + ?wait_async_action( + emqx:publish(Message), + #{?snk_kind := gcp_pubsub_response}, + 5_000 + ), fun(Trace) -> - case QueryMode of - sync -> - ?assertMatch( - [#{reason := killed}], - ?of_kind(gcp_pubsub_request_failed, Trace) - ); - async -> - ?assertMatch( - [#{response := {error, killed}}], - ?of_kind(gcp_pubsub_response, Trace) - ) - end, + ?assertMatch( + [#{response := {error, killed}}], + ?of_kind(gcp_pubsub_response, Trace) + ), ok end ), diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl index 2b2214df0..1b4b4aeb2 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl @@ -532,10 +532,12 @@ t_start_ok(Config) -> }, ?check_trace( begin - ?assertEqual(ok, send_message(Config, SentData)), case QueryMode of - async -> ct:sleep(500); - sync -> ok + async -> + ?assertMatch(ok, send_message(Config, SentData)), + ct:sleep(500); + sync -> + ?assertMatch({ok, 204, _}, send_message(Config, SentData)) end, PersistedData = query_by_clientid(ClientId, Config), Expected = #{ @@ -689,10 +691,12 @@ t_const_timestamp(Config) -> <<"payload">> => Payload, <<"timestamp">> => erlang:system_time(millisecond) }, - ?assertEqual(ok, send_message(Config, SentData)), case QueryMode of - async -> ct:sleep(500); - sync -> ok + async -> + ?assertMatch(ok, send_message(Config, SentData)), + ct:sleep(500); + sync -> + ?assertMatch({ok, 204, _}, send_message(Config, SentData)) end, PersistedData = query_by_clientid(ClientId, Config), Expected = #{foo => <<"123">>}, @@ -745,7 +749,12 @@ t_boolean_variants(Config) -> <<"timestamp">> => erlang:system_time(millisecond), <<"payload">> => Payload }, - ?assertEqual(ok, send_message(Config, SentData)), + case QueryMode of + sync -> + ?assertMatch({ok, 204, _}, send_message(Config, SentData)); + async -> + ?assertMatch(ok, send_message(Config, SentData)) + end, case QueryMode of async -> ct:sleep(500); sync -> ok @@ -841,10 +850,9 @@ t_bad_timestamp(Config) -> ); {sync, false} -> ?assertEqual( - {error, - {unrecoverable_error, [ - {error, {bad_timestamp, <<"bad_timestamp">>}} - ]}}, + {error, [ + {error, {bad_timestamp, <<"bad_timestamp">>}} + ]}, Return ); {sync, true} -> @@ -964,7 +972,7 @@ t_write_failure(Config) -> {error, {resource_error, #{reason := timeout}}}, send_message(Config, SentData) ), - #{?snk_kind := buffer_worker_flush_nack}, + #{?snk_kind := handle_async_reply, action := nack}, 1_000 ); async -> @@ -978,13 +986,11 @@ t_write_failure(Config) -> fun(Trace0) -> case QueryMode of sync -> - Trace = ?of_kind(buffer_worker_flush_nack, Trace0), + Trace = ?of_kind(handle_async_reply, Trace0), ?assertMatch([_ | _], Trace), [#{result := Result} | _] = Trace, ?assert( - {error, {error, {closed, "The connection was lost."}}} =:= Result orelse - {error, {error, closed}} =:= Result orelse - {error, {recoverable_error, {error, econnrefused}}} =:= Result, + not emqx_ee_connector_influxdb:is_unrecoverable_error(Result), #{got => Result} ); async -> @@ -992,11 +998,7 @@ t_write_failure(Config) -> ?assertMatch([#{action := nack} | _], Trace), [#{result := Result} | _] = Trace, ?assert( - {error, {recoverable_error, {closed, "The connection was lost."}}} =:= - Result orelse - {error, {error, closed}} =:= Result orelse - {error, {recoverable_error, econnrefused}} =:= Result orelse - {error, {recoverable_error, noproc}} =:= Result, + not emqx_ee_connector_influxdb:is_unrecoverable_error(Result), #{got => Result} ) end, @@ -1006,7 +1008,6 @@ t_write_failure(Config) -> ok. t_missing_field(Config) -> - QueryMode = ?config(query_mode, Config), BatchSize = ?config(batch_size, Config), IsBatch = BatchSize > 1, {ok, _} = @@ -1034,8 +1035,7 @@ t_missing_field(Config) -> {ok, _} = snabbkaffe:block_until( ?match_n_events(NEvents, #{ - ?snk_kind := influxdb_connector_send_query_error, - mode := QueryMode + ?snk_kind := influxdb_connector_send_query_error }), _Timeout1 = 10_000 ), diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_tests.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_tests.erl new file mode 100644 index 000000000..ce3a0b06f --- /dev/null +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_tests.erl @@ -0,0 +1,328 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +-module(emqx_ee_bridge_influxdb_tests). + +-include_lib("eunit/include/eunit.hrl"). + +-import(emqx_ee_bridge_influxdb, [to_influx_lines/1]). + +-define(INVALID_LINES, [ + " ", + " \n", + " \n\n\n ", + "\n", + " \n\n \n \n", + "measurement", + "measurement ", + "measurement,tag", + "measurement field", + "measurement,tag field", + "measurement,tag field ${timestamp}", + "measurement,tag=", + "measurement,tag=tag1", + "measurement,tag =", + "measurement field=", + "measurement field= ", + "measurement field = ", + "measurement, tag = field = ", + "measurement, tag = field = ", + "measurement, tag = tag_val field = field_val", + "measurement, tag = tag_val field = field_val ${timestamp}", + "measurement,= = ${timestamp}", + "measurement,t=a, f=a, ${timestamp}", + "measurement,t=a,t1=b, f=a,f1=b, ${timestamp}", + "measurement,t=a,t1=b, f=a,f1=b,", + "measurement,t=a, t1=b, f=a,f1=b,", + "measurement,t=a,,t1=b, f=a,f1=b,", + "measurement,t=a,,t1=b f=a,,f1=b", + "measurement,t=a,,t1=b f=a,f1=b ${timestamp}", + "measurement, f=a,f1=b", + "measurement, f=a,f1=b ${timestamp}", + "measurement,, f=a,f1=b ${timestamp}", + "measurement,, f=a,f1=b", + "measurement,, f=a,f1=b,, ${timestamp}", + "measurement f=a,f1=b,, ${timestamp}", + "measurement,t=a f=a,f1=b,, ${timestamp}", + "measurement,t=a f=a,f1=b,, ", + "measurement,t=a f=a,f1=b,,", + "measurement, t=a f=a,f1=b", + "measurement,t=a f=a, f1=b", + "measurement,t=a f=a, f1=b ${timestamp}", + "measurement, t=a f=a, f1=b ${timestamp}", + "measurement,t= a f=a,f1=b ${timestamp}", + "measurement,t= a f=a,f1 =b ${timestamp}", + "measurement, t = a f = a,f1 = b ${timestamp}", + "measurement,t=a f=a,f1=b \n ${timestamp}", + "measurement,t=a \n f=a,f1=b \n ${timestamp}", + "measurement,t=a \n f=a,f1=b \n ", + "\n measurement,t=a \n f=a,f1=b \n ${timestamp}", + "\n measurement,t=a \n f=a,f1=b \n", + %% not escaped backslash in a quoted field value is invalid + "measurement,tag=1 field=\"val\\1\"" +]). + +-define(VALID_LINE_PARSED_PAIRS, [ + {"m1,tag=tag1 field=field1 ${timestamp1}", #{ + measurement => "m1", + tags => [{"tag", "tag1"}], + fields => [{"field", "field1"}], + timestamp => "${timestamp1}" + }}, + {"m2,tag=tag2 field=field2", #{ + measurement => "m2", + tags => [{"tag", "tag2"}], + fields => [{"field", "field2"}], + timestamp => undefined + }}, + {"m3 field=field3 ${timestamp3}", #{ + measurement => "m3", + tags => [], + fields => [{"field", "field3"}], + timestamp => "${timestamp3}" + }}, + {"m4 field=field4", #{ + measurement => "m4", + tags => [], + fields => [{"field", "field4"}], + timestamp => undefined + }}, + {"m5,tag=tag5,tag_a=tag5a,tag_b=tag5b field=field5,field_a=field5a,field_b=field5b ${timestamp5}", + #{ + measurement => "m5", + tags => [{"tag", "tag5"}, {"tag_a", "tag5a"}, {"tag_b", "tag5b"}], + fields => [{"field", "field5"}, {"field_a", "field5a"}, {"field_b", "field5b"}], + timestamp => "${timestamp5}" + }}, + {"m6,tag=tag6,tag_a=tag6a,tag_b=tag6b field=field6,field_a=field6a,field_b=field6b", #{ + measurement => "m6", + tags => [{"tag", "tag6"}, {"tag_a", "tag6a"}, {"tag_b", "tag6b"}], + fields => [{"field", "field6"}, {"field_a", "field6a"}, {"field_b", "field6b"}], + timestamp => undefined + }}, + {"m7,tag=tag7,tag_a=\"tag7a\",tag_b=tag7b field=\"field7\",field_a=field7a,field_b=\"field7b\"", + #{ + measurement => "m7", + tags => [{"tag", "tag7"}, {"tag_a", "\"tag7a\""}, {"tag_b", "tag7b"}], + fields => [{"field", "field7"}, {"field_a", "field7a"}, {"field_b", "field7b"}], + timestamp => undefined + }}, + {"m8,tag=tag8,tag_a=\"tag8a\",tag_b=tag8b field=\"field8\",field_a=field8a,field_b=\"field8b\" ${timestamp8}", + #{ + measurement => "m8", + tags => [{"tag", "tag8"}, {"tag_a", "\"tag8a\""}, {"tag_b", "tag8b"}], + fields => [{"field", "field8"}, {"field_a", "field8a"}, {"field_b", "field8b"}], + timestamp => "${timestamp8}" + }}, + {"m9,tag=tag9,tag_a=\"tag9a\",tag_b=tag9b field=\"field9\",field_a=field9a,field_b=\"\" ${timestamp9}", + #{ + measurement => "m9", + tags => [{"tag", "tag9"}, {"tag_a", "\"tag9a\""}, {"tag_b", "tag9b"}], + fields => [{"field", "field9"}, {"field_a", "field9a"}, {"field_b", ""}], + timestamp => "${timestamp9}" + }}, + {"m10 field=\"\" ${timestamp10}", #{ + measurement => "m10", + tags => [], + fields => [{"field", ""}], + timestamp => "${timestamp10}" + }} +]). + +-define(VALID_LINE_EXTRA_SPACES_PARSED_PAIRS, [ + {"\n m1,tag=tag1 field=field1 ${timestamp1} \n", #{ + measurement => "m1", + tags => [{"tag", "tag1"}], + fields => [{"field", "field1"}], + timestamp => "${timestamp1}" + }}, + {" m2,tag=tag2 field=field2 ", #{ + measurement => "m2", + tags => [{"tag", "tag2"}], + fields => [{"field", "field2"}], + timestamp => undefined + }}, + {" m3 field=field3 ${timestamp3} ", #{ + measurement => "m3", + tags => [], + fields => [{"field", "field3"}], + timestamp => "${timestamp3}" + }}, + {" \n m4 field=field4\n ", #{ + measurement => "m4", + tags => [], + fields => [{"field", "field4"}], + timestamp => undefined + }}, + {" \n m5,tag=tag5,tag_a=tag5a,tag_b=tag5b field=field5,field_a=field5a,field_b=field5b ${timestamp5} \n", + #{ + measurement => "m5", + tags => [{"tag", "tag5"}, {"tag_a", "tag5a"}, {"tag_b", "tag5b"}], + fields => [{"field", "field5"}, {"field_a", "field5a"}, {"field_b", "field5b"}], + timestamp => "${timestamp5}" + }}, + {" m6,tag=tag6,tag_a=tag6a,tag_b=tag6b field=field6,field_a=field6a,field_b=field6b\n ", #{ + measurement => "m6", + tags => [{"tag", "tag6"}, {"tag_a", "tag6a"}, {"tag_b", "tag6b"}], + fields => [{"field", "field6"}, {"field_a", "field6a"}, {"field_b", "field6b"}], + timestamp => undefined + }} +]). + +-define(VALID_LINE_PARSED_ESCAPED_CHARS_PAIRS, [ + {"m\\ =1\\,,\\,tag\\ \\==\\=tag\\ 1\\, \\,fie\\ ld\\ =\\ field\\,1 ${timestamp1}", #{ + measurement => "m =1,", + tags => [{",tag =", "=tag 1,"}], + fields => [{",fie ld ", " field,1"}], + timestamp => "${timestamp1}" + }}, + {"m2,tag=tag2 field=\"field \\\"2\\\",\n\"", #{ + measurement => "m2", + tags => [{"tag", "tag2"}], + fields => [{"field", "field \"2\",\n"}], + timestamp => undefined + }}, + {"m\\ 3 field=\"field3\" ${payload.timestamp\\ 3}", #{ + measurement => "m 3", + tags => [], + fields => [{"field", "field3"}], + timestamp => "${payload.timestamp 3}" + }}, + {"m4 field=\"\\\"field\\\\4\\\"\"", #{ + measurement => "m4", + tags => [], + fields => [{"field", "\"field\\4\""}], + timestamp => undefined + }}, + {"m5\\,mA,tag=\\=tag5\\=,\\,tag_a\\,=tag\\ 5a,tag_b=tag5b \\ field\\ =field5,field\\ _\\ a=field5a,\\,field_b\\ =\\=\\,\\ field5b ${timestamp5}", + #{ + measurement => "m5,mA", + tags => [{"tag", "=tag5="}, {",tag_a,", "tag 5a"}, {"tag_b", "tag5b"}], + fields => [ + {" field ", "field5"}, {"field _ a", "field5a"}, {",field_b ", "=, field5b"} + ], + timestamp => "${timestamp5}" + }}, + {"m6,tag=tag6,tag_a=tag6a,tag_b=tag6b field=\"field6\",field_a=\"field6a\",field_b=\"field6b\"", + #{ + measurement => "m6", + tags => [{"tag", "tag6"}, {"tag_a", "tag6a"}, {"tag_b", "tag6b"}], + fields => [{"field", "field6"}, {"field_a", "field6a"}, {"field_b", "field6b"}], + timestamp => undefined + }}, + {"\\ \\ m7\\ \\ ,tag=\\ tag\\,7\\ ,tag_a=\"tag7a\",tag_b\\,tag1=tag7b field=\"field7\",field_a=field7a,field_b=\"field7b\\\\\n\"", + #{ + measurement => " m7 ", + tags => [{"tag", " tag,7 "}, {"tag_a", "\"tag7a\""}, {"tag_b,tag1", "tag7b"}], + fields => [{"field", "field7"}, {"field_a", "field7a"}, {"field_b", "field7b\\\n"}], + timestamp => undefined + }}, + {"m8,tag=tag8,tag_a=\"tag8a\",tag_b=tag8b field=\"field8\",field_a=field8a,field_b=\"\\\"field\\\" = 8b\" ${timestamp8}", + #{ + measurement => "m8", + tags => [{"tag", "tag8"}, {"tag_a", "\"tag8a\""}, {"tag_b", "tag8b"}], + fields => [{"field", "field8"}, {"field_a", "field8a"}, {"field_b", "\"field\" = 8b"}], + timestamp => "${timestamp8}" + }}, + {"m\\9,tag=tag9,tag_a=\"tag9a\",tag_b=tag9b field\\=field=\"field9\",field_a=field9a,field_b=\"\" ${timestamp9}", + #{ + measurement => "m\\9", + tags => [{"tag", "tag9"}, {"tag_a", "\"tag9a\""}, {"tag_b", "tag9b"}], + fields => [{"field=field", "field9"}, {"field_a", "field9a"}, {"field_b", ""}], + timestamp => "${timestamp9}" + }}, + {"m\\,10 \"field\\\\\"=\"\" ${timestamp10}", #{ + measurement => "m,10", + tags => [], + %% backslash should not be un-escaped in tag key + fields => [{"\"field\\\\\"", ""}], + timestamp => "${timestamp10}" + }} +]). + +-define(VALID_LINE_PARSED_ESCAPED_CHARS_EXTRA_SPACES_PAIRS, [ + {" \n m\\ =1\\,,\\,tag\\ \\==\\=tag\\ 1\\, \\,fie\\ ld\\ =\\ field\\,1 ${timestamp1} ", #{ + measurement => "m =1,", + tags => [{",tag =", "=tag 1,"}], + fields => [{",fie ld ", " field,1"}], + timestamp => "${timestamp1}" + }}, + {" m2,tag=tag2 field=\"field \\\"2\\\",\n\" ", #{ + measurement => "m2", + tags => [{"tag", "tag2"}], + fields => [{"field", "field \"2\",\n"}], + timestamp => undefined + }}, + {" m\\ 3 field=\"field3\" ${payload.timestamp\\ 3} ", #{ + measurement => "m 3", + tags => [], + fields => [{"field", "field3"}], + timestamp => "${payload.timestamp 3}" + }}, + {" m4 field=\"\\\"field\\\\4\\\"\" ", #{ + measurement => "m4", + tags => [], + fields => [{"field", "\"field\\4\""}], + timestamp => undefined + }}, + {" m5\\,mA,tag=\\=tag5\\=,\\,tag_a\\,=tag\\ 5a,tag_b=tag5b \\ field\\ =field5,field\\ _\\ a=field5a,\\,field_b\\ =\\=\\,\\ field5b ${timestamp5} ", + #{ + measurement => "m5,mA", + tags => [{"tag", "=tag5="}, {",tag_a,", "tag 5a"}, {"tag_b", "tag5b"}], + fields => [ + {" field ", "field5"}, {"field _ a", "field5a"}, {",field_b ", "=, field5b"} + ], + timestamp => "${timestamp5}" + }}, + {" m6,tag=tag6,tag_a=tag6a,tag_b=tag6b field=\"field6\",field_a=\"field6a\",field_b=\"field6b\" ", + #{ + measurement => "m6", + tags => [{"tag", "tag6"}, {"tag_a", "tag6a"}, {"tag_b", "tag6b"}], + fields => [{"field", "field6"}, {"field_a", "field6a"}, {"field_b", "field6b"}], + timestamp => undefined + }} +]). + +invalid_write_syntax_line_test_() -> + [?_assertThrow(_, to_influx_lines(L)) || L <- ?INVALID_LINES]. + +invalid_write_syntax_multiline_test_() -> + LinesList = [ + join("\n", ?INVALID_LINES), + join("\n\n\n", ?INVALID_LINES), + join("\n\n", lists:reverse(?INVALID_LINES)) + ], + [?_assertThrow(_, to_influx_lines(Lines)) || Lines <- LinesList]. + +valid_write_syntax_test_() -> + test_pairs(?VALID_LINE_PARSED_PAIRS). + +valid_write_syntax_with_extra_spaces_test_() -> + test_pairs(?VALID_LINE_EXTRA_SPACES_PARSED_PAIRS). + +valid_write_syntax_escaped_chars_test_() -> + test_pairs(?VALID_LINE_PARSED_ESCAPED_CHARS_PAIRS). + +valid_write_syntax_escaped_chars_with_extra_spaces_test_() -> + test_pairs(?VALID_LINE_PARSED_ESCAPED_CHARS_EXTRA_SPACES_PAIRS). + +test_pairs(PairsList) -> + {Lines, AllExpected} = lists:unzip(PairsList), + JoinedLines = join("\n", Lines), + JoinedLines1 = join("\n\n\n", Lines), + JoinedLines2 = join("\n\n", lists:reverse(Lines)), + SingleLineTests = + [ + ?_assertEqual([Expected], to_influx_lines(Line)) + || {Line, Expected} <- PairsList + ], + JoinedLinesTests = + [ + ?_assertEqual(AllExpected, to_influx_lines(JoinedLines)), + ?_assertEqual(AllExpected, to_influx_lines(JoinedLines1)), + ?_assertEqual(lists:reverse(AllExpected), to_influx_lines(JoinedLines2)) + ], + SingleLineTests ++ JoinedLinesTests. + +join(Sep, LinesList) -> + lists:flatten(lists:join(Sep, LinesList)). diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl index f81571223..9850c9529 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl @@ -26,7 +26,8 @@ group_tests() -> [ t_setup_via_config_and_publish, t_setup_via_http_api_and_publish, - t_payload_template + t_payload_template, + t_collection_template ]. groups() -> @@ -302,3 +303,24 @@ t_payload_template(Config) -> find_all(Config) ), ok. + +t_collection_template(Config) -> + {ok, _} = create_bridge( + Config, + #{ + <<"payload_template">> => <<"{\"foo\": \"${clientid}\"}">>, + <<"collection">> => <<"${mycollectionvar}">> + } + ), + Val = erlang:unique_integer(), + ClientId = emqx_guid:to_hexstr(emqx_guid:gen()), + ok = send_message(Config, #{ + key => Val, + clientid => ClientId, + mycollectionvar => <<"mycol">> + }), + ?assertMatch( + {ok, [#{<<"foo">> := ClientId}]}, + find_all(Config) + ), + ok. diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_rocketmq_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_rocketmq_SUITE.erl new file mode 100644 index 000000000..cd02b65d0 --- /dev/null +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_rocketmq_SUITE.erl @@ -0,0 +1,267 @@ +%%-------------------------------------------------------------------- +% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_ee_bridge_rocketmq_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +% Bridge defaults +-define(TOPIC, "TopicTest"). +-define(BATCH_SIZE, 10). +-define(PAYLOAD, <<"HELLO">>). + +-define(GET_CONFIG(KEY__, CFG__), proplists:get_value(KEY__, CFG__)). + +%%------------------------------------------------------------------------------ +%% CT boilerplate +%%------------------------------------------------------------------------------ + +all() -> + [ + {group, with_batch}, + {group, without_batch} + ]. + +groups() -> + TCs = emqx_common_test_helpers:all(?MODULE), + [ + {with_batch, TCs}, + {without_batch, TCs} + ]. + +init_per_group(with_batch, Config0) -> + Config = [{batch_size, ?BATCH_SIZE} | Config0], + common_init(Config); +init_per_group(without_batch, Config0) -> + Config = [{batch_size, 1} | Config0], + common_init(Config); +init_per_group(_Group, Config) -> + Config. + +end_per_group(Group, Config) when Group =:= with_batch; Group =:= without_batch -> + ProxyHost = ?config(proxy_host, Config), + ProxyPort = ?config(proxy_port, Config), + emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), + ok; +end_per_group(_Group, _Config) -> + ok. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + emqx_mgmt_api_test_util:end_suite(), + ok = emqx_common_test_helpers:stop_apps([emqx_bridge, emqx_conf]), + ok. + +init_per_testcase(_Testcase, Config) -> + delete_bridge(Config), + Config. + +end_per_testcase(_Testcase, Config) -> + ProxyHost = ?config(proxy_host, Config), + ProxyPort = ?config(proxy_port, Config), + emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), + ok = snabbkaffe:stop(), + delete_bridge(Config), + ok. + +%%------------------------------------------------------------------------------ +%% Helper fns +%%------------------------------------------------------------------------------ + +common_init(ConfigT) -> + BridgeType = <<"rocketmq">>, + Host = os:getenv("ROCKETMQ_HOST", "toxiproxy"), + Port = list_to_integer(os:getenv("ROCKETMQ_PORT", "9876")), + + Config0 = [ + {host, Host}, + {port, Port}, + {query_mode, sync}, + {proxy_name, "rocketmq"} + | ConfigT + ], + + case emqx_common_test_helpers:is_tcp_server_available(Host, Port) of + true -> + % Setup toxiproxy + ProxyHost = os:getenv("PROXY_HOST", "toxiproxy"), + ProxyPort = list_to_integer(os:getenv("PROXY_PORT", "8474")), + emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), + % Ensure EE bridge module is loaded + _ = application:load(emqx_ee_bridge), + _ = emqx_ee_bridge:module_info(), + ok = emqx_common_test_helpers:start_apps([emqx_conf, emqx_bridge]), + emqx_mgmt_api_test_util:init_suite(), + {Name, RocketMQConf} = rocketmq_config(BridgeType, Config0), + Config = + [ + {rocketmq_config, RocketMQConf}, + {rocketmq_bridge_type, BridgeType}, + {rocketmq_name, Name}, + {proxy_host, ProxyHost}, + {proxy_port, ProxyPort} + | Config0 + ], + Config; + false -> + case os:getenv("IS_CI") of + false -> + {skip, no_rocketmq}; + _ -> + throw(no_rocketmq) + end + end. + +rocketmq_config(BridgeType, Config) -> + Port = integer_to_list(?GET_CONFIG(port, Config)), + Server = ?GET_CONFIG(host, Config) ++ ":" ++ Port, + Name = atom_to_binary(?MODULE), + BatchSize = ?config(batch_size, Config), + QueryMode = ?config(query_mode, Config), + ConfigString = + io_lib:format( + "bridges.~s.~s {\n" + " enable = true\n" + " server = ~p\n" + " topic = ~p\n" + " resource_opts = {\n" + " request_timeout = 1500ms\n" + " batch_size = ~b\n" + " query_mode = ~s\n" + " }\n" + "}", + [ + BridgeType, + Name, + Server, + ?TOPIC, + BatchSize, + QueryMode + ] + ), + {Name, parse_and_check(ConfigString, BridgeType, Name)}. + +parse_and_check(ConfigString, BridgeType, Name) -> + {ok, RawConf} = hocon:binary(ConfigString, #{format => map}), + hocon_tconf:check_plain(emqx_bridge_schema, RawConf, #{required => false, atom_key => false}), + #{<<"bridges">> := #{BridgeType := #{Name := Config}}} = RawConf, + Config. + +create_bridge(Config) -> + BridgeType = ?GET_CONFIG(rocketmq_bridge_type, Config), + Name = ?GET_CONFIG(rocketmq_name, Config), + RocketMQConf = ?GET_CONFIG(rocketmq_config, Config), + emqx_bridge:create(BridgeType, Name, RocketMQConf). + +delete_bridge(Config) -> + BridgeType = ?GET_CONFIG(rocketmq_bridge_type, Config), + Name = ?GET_CONFIG(rocketmq_name, Config), + emqx_bridge:remove(BridgeType, Name). + +create_bridge_http(Params) -> + Path = emqx_mgmt_api_test_util:api_path(["bridges"]), + AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + case emqx_mgmt_api_test_util:request_api(post, Path, "", AuthHeader, Params) of + {ok, Res} -> {ok, emqx_json:decode(Res, [return_maps])}; + Error -> Error + end. + +send_message(Config, Payload) -> + Name = ?GET_CONFIG(rocketmq_name, Config), + BridgeType = ?GET_CONFIG(rocketmq_bridge_type, Config), + BridgeID = emqx_bridge_resource:bridge_id(BridgeType, Name), + emqx_bridge:send_message(BridgeID, Payload). + +query_resource(Config, Request) -> + Name = ?GET_CONFIG(rocketmq_name, Config), + BridgeType = ?GET_CONFIG(rocketmq_bridge_type, Config), + ResourceID = emqx_bridge_resource:resource_id(BridgeType, Name), + emqx_resource:query(ResourceID, Request, #{timeout => 500}). + +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ + +t_setup_via_config_and_publish(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + SentData = #{payload => ?PAYLOAD}, + ?check_trace( + begin + ?wait_async_action( + ?assertEqual(ok, send_message(Config, SentData)), + #{?snk_kind := rocketmq_connector_query_return}, + 10_000 + ), + ok + end, + fun(Trace0) -> + Trace = ?of_kind(rocketmq_connector_query_return, Trace0), + ?assertMatch([#{result := ok}], Trace), + ok + end + ), + ok. + +t_setup_via_http_api_and_publish(Config) -> + BridgeType = ?GET_CONFIG(rocketmq_bridge_type, Config), + Name = ?GET_CONFIG(rocketmq_name, Config), + RocketMQConf = ?GET_CONFIG(rocketmq_config, Config), + RocketMQConf2 = RocketMQConf#{ + <<"name">> => Name, + <<"type">> => BridgeType + }, + ?assertMatch( + {ok, _}, + create_bridge_http(RocketMQConf2) + ), + SentData = #{payload => ?PAYLOAD}, + ?check_trace( + begin + ?wait_async_action( + ?assertEqual(ok, send_message(Config, SentData)), + #{?snk_kind := rocketmq_connector_query_return}, + 10_000 + ), + ok + end, + fun(Trace0) -> + Trace = ?of_kind(rocketmq_connector_query_return, Trace0), + ?assertMatch([#{result := ok}], Trace), + ok + end + ), + ok. + +t_get_status(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + + Name = ?GET_CONFIG(rocketmq_name, Config), + BridgeType = ?GET_CONFIG(rocketmq_bridge_type, Config), + ResourceID = emqx_bridge_resource:resource_id(BridgeType, Name), + + ?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceID)), + ok. + +t_simple_query(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + Request = {send_message, #{message => <<"Hello">>}}, + Result = query_resource(Config, Request), + ?assertEqual(ok, Result), + ok. diff --git a/lib-ee/emqx_ee_connector/docker-ct b/lib-ee/emqx_ee_connector/docker-ct index 3db090939..446c8ee4e 100644 --- a/lib-ee/emqx_ee_connector/docker-ct +++ b/lib-ee/emqx_ee_connector/docker-ct @@ -1,3 +1,4 @@ toxiproxy influxdb clickhouse +cassandra diff --git a/lib-ee/emqx_ee_connector/include/emqx_ee_connector.hrl b/lib-ee/emqx_ee_connector/include/emqx_ee_connector.hrl index 4b6fbbd92..2a91d2524 100644 --- a/lib-ee/emqx_ee_connector/include/emqx_ee_connector.hrl +++ b/lib-ee/emqx_ee_connector/include/emqx_ee_connector.hrl @@ -3,3 +3,4 @@ %%------------------------------------------------------------------- -define(INFLUXDB_DEFAULT_PORT, 8086). +-define(CASSANDRA_DEFAULT_PORT, 9042). diff --git a/lib-ee/emqx_ee_connector/rebar.config b/lib-ee/emqx_ee_connector/rebar.config index 4c1797e34..d49ce59c0 100644 --- a/lib-ee/emqx_ee_connector/rebar.config +++ b/lib-ee/emqx_ee_connector/rebar.config @@ -5,6 +5,7 @@ {tdengine, {git, "https://github.com/emqx/tdengine-client-erl", {tag, "0.1.5"}}}, {clickhouse, {git, "https://github.com/emqx/clickhouse-client-erl", {tag, "0.3"}}}, {erlcloud, {git, "https://github.com/emqx/erlcloud.git", {tag,"3.5.16-emqx-1"}}}, + {rocketmq, {git, "https://github.com/emqx/rocketmq-client-erl.git", {tag, "v0.5.1"}}}, {emqx, {path, "../../apps/emqx"}} ]}. diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src b/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src index a005071da..7a1da983f 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src @@ -11,7 +11,9 @@ wolff, brod, clickhouse, - erlcloud + erlcloud, + rocketmq, + ecql ]}, {env, []}, {modules, []}, diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_cassa.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_cassa.erl new file mode 100644 index 000000000..a6a77f233 --- /dev/null +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_cassa.erl @@ -0,0 +1,409 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_ee_connector_cassa). + +-behaviour(emqx_resource). + +-include_lib("emqx_connector/include/emqx_connector.hrl"). +-include_lib("emqx_ee_connector/include/emqx_ee_connector.hrl"). +-include_lib("typerefl/include/types.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +%% schema +-export([roots/0, fields/1]). + +%% callbacks of behaviour emqx_resource +-export([ + callback_mode/0, + on_start/2, + on_stop/2, + on_query/3, + %% TODO: not_supported_now + %%on_batch_query/3, + on_get_status/2 +]). + +%% callbacks of ecpool +-export([ + connect/1, + prepare_cql_to_conn/2 +]). + +%% callbacks for query executing +-export([query/3, prepared_query/3]). + +-export([do_get_status/1]). + +-type prepares() :: #{atom() => binary()}. +-type params_tokens() :: #{atom() => list()}. + +-type state() :: + #{ + poolname := atom(), + prepare_cql := prepares(), + params_tokens := params_tokens(), + %% returned by ecql:prepare/2 + prepare_statement := binary() + }. + +-define(DEFAULT_SERVER_OPTION, #{default_port => ?CASSANDRA_DEFAULT_PORT}). + +%%-------------------------------------------------------------------- +%% schema + +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]. + +fields(config) -> + cassandra_db_fields() ++ + emqx_connector_schema_lib:ssl_fields() ++ + emqx_connector_schema_lib:prepare_statement_fields(). + +cassandra_db_fields() -> + [ + {servers, servers()}, + {keyspace, fun keyspace/1}, + {pool_size, fun emqx_connector_schema_lib:pool_size/1}, + {username, fun emqx_connector_schema_lib:username/1}, + {password, fun emqx_connector_schema_lib:password/1}, + {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1} + ]. + +servers() -> + Meta = #{desc => ?DESC("servers")}, + emqx_schema:servers_sc(Meta, ?DEFAULT_SERVER_OPTION). + +keyspace(type) -> binary(); +keyspace(desc) -> ?DESC("keyspace"); +keyspace(required) -> true; +keyspace(_) -> undefined. + +%%-------------------------------------------------------------------- +%% callbacks for emqx_resource + +callback_mode() -> always_sync. + +-spec on_start(binary(), hoconsc:config()) -> {ok, state()} | {error, _}. +on_start( + InstId, + #{ + servers := Servers, + keyspace := Keyspace, + username := Username, + pool_size := PoolSize, + ssl := SSL + } = Config +) -> + ?SLOG(info, #{ + msg => "starting_cassandra_connector", + connector => InstId, + config => emqx_misc:redact(Config) + }), + + Options = [ + {nodes, emqx_schema:parse_servers(Servers, ?DEFAULT_SERVER_OPTION)}, + {username, Username}, + {password, emqx_secret:wrap(maps:get(password, Config, ""))}, + {keyspace, Keyspace}, + {auto_reconnect, ?AUTO_RECONNECT_INTERVAL}, + {pool_size, PoolSize} + ], + + SslOpts = + case maps:get(enable, SSL) of + true -> + [ + %% note: type defined at ecql:option/0 + {ssl, emqx_tls_lib:to_client_opts(SSL)} + ]; + false -> + [] + end, + %% use InstaId of binary type as Pool name, which is supported in ecpool. + PoolName = InstId, + Prepares = parse_prepare_cql(Config), + InitState = #{poolname => PoolName, prepare_statement => #{}}, + State = maps:merge(InitState, Prepares), + case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of + ok -> + {ok, init_prepare(State)}; + {error, Reason} -> + ?tp( + cassandra_connector_start_failed, + #{error => Reason} + ), + {error, Reason} + end. + +on_stop(InstId, #{poolname := PoolName}) -> + ?SLOG(info, #{ + msg => "stopping_cassandra_connector", + connector => InstId + }), + emqx_plugin_libs_pool:stop_pool(PoolName). + +-type request() :: + % emqx_bridge.erl + {send_message, Params :: map()} + % common query + | {query, SQL :: binary()} + | {query, SQL :: binary(), Params :: map()}. + +-spec on_query( + emqx_resource:resource_id(), + request(), + state() +) -> ok | {ok, ecql:cql_result()} | {error, {recoverable_error | unrecoverable_error, term()}}. +on_query( + InstId, + Request, + #{poolname := PoolName} = State +) -> + {Type, PreparedKeyOrSQL, Params} = parse_request_to_cql(Request), + ?tp( + debug, + cassandra_connector_received_cql_query, + #{ + connector => InstId, + type => Type, + params => Params, + prepared_key_or_cql => PreparedKeyOrSQL, + state => State + } + ), + {PreparedKeyOrSQL1, Data} = proc_cql_params(Type, PreparedKeyOrSQL, Params, State), + Res = exec_cql_query(InstId, PoolName, Type, PreparedKeyOrSQL1, Data), + handle_result(Res). + +parse_request_to_cql({send_message, Params}) -> + {prepared_query, _Key = send_message, Params}; +parse_request_to_cql({query, SQL}) -> + parse_request_to_cql({query, SQL, #{}}); +parse_request_to_cql({query, SQL, Params}) -> + {query, SQL, Params}. + +proc_cql_params( + prepared_query, + PreparedKey0, + Params, + #{prepare_statement := Prepares, params_tokens := ParamsTokens} +) -> + PreparedKey = maps:get(PreparedKey0, Prepares), + Tokens = maps:get(PreparedKey0, ParamsTokens), + {PreparedKey, assign_type_for_params(emqx_plugin_libs_rule:proc_sql(Tokens, Params))}; +proc_cql_params(query, SQL, Params, _State) -> + {SQL1, Tokens} = emqx_plugin_libs_rule:preproc_sql(SQL, '?'), + {SQL1, assign_type_for_params(emqx_plugin_libs_rule:proc_sql(Tokens, Params))}. + +exec_cql_query(InstId, PoolName, Type, PreparedKey, Data) when + Type == query; Type == prepared_query +-> + case ecpool:pick_and_do(PoolName, {?MODULE, Type, [PreparedKey, Data]}, no_handover) of + {error, Reason} = Result -> + ?tp( + error, + cassandra_connector_query_return, + #{connector => InstId, error => Reason} + ), + Result; + Result -> + ?tp(debug, cassandra_connector_query_return, #{result => Result}), + Result + end. + +on_get_status(_InstId, #{poolname := Pool} = State) -> + case emqx_plugin_libs_pool:health_check_ecpool_workers(Pool, fun ?MODULE:do_get_status/1) of + true -> + case do_check_prepares(State) of + ok -> + connected; + {ok, NState} -> + %% return new state with prepared statements + {connected, NState}; + false -> + %% do not log error, it is logged in prepare_cql_to_conn + connecting + end; + false -> + connecting + end. + +do_get_status(Conn) -> + ok == element(1, ecql:query(Conn, "SELECT count(1) AS T FROM system.local")). + +do_check_prepares(#{prepare_cql := Prepares}) when is_map(Prepares) -> + ok; +do_check_prepares(State = #{poolname := PoolName, prepare_cql := {error, Prepares}}) -> + %% retry to prepare + case prepare_cql(Prepares, PoolName) of + {ok, Sts} -> + %% remove the error + {ok, State#{prepare_cql => Prepares, prepare_statement := Sts}}; + _Error -> + false + end. + +%%-------------------------------------------------------------------- +%% callbacks query + +query(Conn, SQL, Params) -> + ecql:query(Conn, SQL, Params). + +prepared_query(Conn, PreparedKey, Params) -> + ecql:execute(Conn, PreparedKey, Params). + +%%-------------------------------------------------------------------- +%% callbacks for ecpool + +connect(Opts) -> + case ecql:connect(conn_opts(Opts)) of + {ok, _Conn} = Ok -> + Ok; + {error, Reason} -> + {error, Reason} + end. + +conn_opts(Opts) -> + conn_opts(Opts, []). + +conn_opts([], Acc) -> + Acc; +conn_opts([{password, Password} | Opts], Acc) -> + conn_opts(Opts, [{password, emqx_secret:unwrap(Password)} | Acc]); +conn_opts([Opt | Opts], Acc) -> + conn_opts(Opts, [Opt | Acc]). + +%%-------------------------------------------------------------------- +%% prepare + +%% XXX: hardcode +%% note: the `cql` param is passed by emqx_ee_bridge_cassa +parse_prepare_cql(#{cql := SQL}) -> + parse_prepare_cql([{send_message, SQL}], #{}, #{}); +parse_prepare_cql(_) -> + #{prepare_cql => #{}, params_tokens => #{}}. + +parse_prepare_cql([{Key, H} | T], Prepares, Tokens) -> + {PrepareSQL, ParamsTokens} = emqx_plugin_libs_rule:preproc_sql(H, '?'), + parse_prepare_cql( + T, Prepares#{Key => PrepareSQL}, Tokens#{Key => ParamsTokens} + ); +parse_prepare_cql([], Prepares, Tokens) -> + #{ + prepare_cql => Prepares, + params_tokens => Tokens + }. + +init_prepare(State = #{prepare_cql := Prepares, poolname := PoolName}) -> + case maps:size(Prepares) of + 0 -> + State; + _ -> + case prepare_cql(Prepares, PoolName) of + {ok, Sts} -> + State#{prepare_statement := Sts}; + Error -> + ?tp( + error, + cassandra_prepare_cql_failed, + #{prepares => Prepares, reason => Error} + ), + %% mark the prepare_cql as failed + State#{prepare_cql => {error, Prepares}} + end + end. + +prepare_cql(Prepares, PoolName) when is_map(Prepares) -> + prepare_cql(maps:to_list(Prepares), PoolName); +prepare_cql(Prepares, PoolName) -> + case do_prepare_cql(Prepares, PoolName) of + {ok, _Sts} = Ok -> + %% prepare for reconnect + ecpool:add_reconnect_callback(PoolName, {?MODULE, prepare_cql_to_conn, [Prepares]}), + Ok; + Error -> + Error + end. + +do_prepare_cql(Prepares, PoolName) -> + do_prepare_cql(ecpool:workers(PoolName), Prepares, PoolName, #{}). + +do_prepare_cql([{_Name, Worker} | T], Prepares, PoolName, _LastSts) -> + {ok, Conn} = ecpool_worker:client(Worker), + case prepare_cql_to_conn(Conn, Prepares) of + {ok, Sts} -> + do_prepare_cql(T, Prepares, PoolName, Sts); + Error -> + Error + end; +do_prepare_cql([], _Prepares, _PoolName, LastSts) -> + {ok, LastSts}. + +prepare_cql_to_conn(Conn, Prepares) -> + prepare_cql_to_conn(Conn, Prepares, #{}). + +prepare_cql_to_conn(Conn, [], Statements) when is_pid(Conn) -> {ok, Statements}; +prepare_cql_to_conn(Conn, [{Key, SQL} | PrepareList], Statements) when is_pid(Conn) -> + ?SLOG(info, #{msg => "cassandra_prepare_cql", name => Key, prepare_cql => SQL}), + case ecql:prepare(Conn, Key, SQL) of + {ok, Statement} -> + prepare_cql_to_conn(Conn, PrepareList, Statements#{Key => Statement}); + {error, Error} = Other -> + ?SLOG(error, #{ + msg => "cassandra_prepare_cql_failed", + worker_pid => Conn, + name => Key, + prepare_cql => SQL, + error => Error + }), + Other + end. + +handle_result({error, disconnected}) -> + {error, {recoverable_error, disconnected}}; +handle_result({error, Error}) -> + {error, {unrecoverable_error, Error}}; +handle_result(Res) -> + Res. + +%%-------------------------------------------------------------------- +%% utils + +%% see ecql driver requirements +assign_type_for_params(Params) -> + assign_type_for_params(Params, []). + +assign_type_for_params([], Acc) -> + lists:reverse(Acc); +assign_type_for_params([Param | More], Acc) -> + assign_type_for_params(More, [maybe_assign_type(Param) | Acc]). + +maybe_assign_type(true) -> + {int, 1}; +maybe_assign_type(false) -> + {int, 0}; +maybe_assign_type(V) when is_binary(V); is_list(V); is_atom(V) -> V; +maybe_assign_type(V) when is_integer(V) -> + %% The max value of signed int(4) is 2147483647 + case V > 2147483647 orelse V < -2147483647 of + true -> {bigint, V}; + false -> {int, V} + end; +maybe_assign_type(V) when is_float(V) -> {double, V}; +maybe_assign_type(V) -> + V. diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_dynamo.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_dynamo.erl index 957706f6a..4703e0a21 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_dynamo.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_dynamo.erl @@ -31,8 +31,7 @@ connect/1, do_get_status/1, do_async_reply/2, - worker_do_query/4, - worker_do_get_status/1 + worker_do_query/4 ]). -import(hoconsc, [mk/2, enum/1, ref/2]). @@ -165,18 +164,14 @@ on_get_status(_InstanceId, #{poolname := Pool}) -> Health = emqx_plugin_libs_pool:health_check_ecpool_workers(Pool, fun ?MODULE:do_get_status/1), status_result(Health). -do_get_status(Conn) -> +do_get_status(_Conn) -> %% because the dynamodb driver connection process is the ecpool worker self %% so we must call the checker function inside the worker - ListTables = ecpool_worker:exec(Conn, {?MODULE, worker_do_get_status, []}, infinity), - case ListTables of + case erlcloud_ddb2:list_tables() of {ok, _} -> true; _ -> false end. -worker_do_get_status(_) -> - erlcloud_ddb2:list_tables(). - status_result(_Status = true) -> connected; status_result(_Status = false) -> connecting. diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 5c99a23a8..afe17cae6 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -35,6 +35,9 @@ desc/1 ]). +%% only for test +-export([is_unrecoverable_error/1]). + -type ts_precision() :: ns | us | ms | s. %% influxdb servers don't need parse @@ -655,12 +658,6 @@ str(S) when is_list(S) -> is_unrecoverable_error({error, {unrecoverable_error, _}}) -> true; -is_unrecoverable_error({error, {recoverable_error, _}}) -> - false; -is_unrecoverable_error({error, {error, econnrefused}}) -> - false; -is_unrecoverable_error({error, econnrefused}) -> - false; is_unrecoverable_error(_) -> false. diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_mongodb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_mongodb.erl index b1327fef6..8df77fbe0 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_mongodb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_mongodb.erl @@ -35,8 +35,11 @@ on_start(InstanceId, Config) -> {ok, ConnectorState} -> PayloadTemplate0 = maps:get(payload_template, Config, undefined), PayloadTemplate = preprocess_template(PayloadTemplate0), + CollectionTemplateSource = maps:get(collection, Config), + CollectionTemplate = preprocess_template(CollectionTemplateSource), State = #{ payload_template => PayloadTemplate, + collection_template => CollectionTemplate, connector_state => ConnectorState }, {ok, State}; @@ -50,10 +53,14 @@ on_stop(InstanceId, _State = #{connector_state := ConnectorState}) -> on_query(InstanceId, {send_message, Message0}, State) -> #{ payload_template := PayloadTemplate, + collection_template := CollectionTemplate, connector_state := ConnectorState } = State, + NewConnectorState = ConnectorState#{ + collection => emqx_plugin_libs_rule:proc_tmpl(CollectionTemplate, Message0) + }, Message = render_message(PayloadTemplate, Message0), - emqx_connector_mongo:on_query(InstanceId, {send_message, Message}, ConnectorState); + emqx_connector_mongo:on_query(InstanceId, {send_message, Message}, NewConnectorState); on_query(InstanceId, Request, _State = #{connector_state := ConnectorState}) -> emqx_connector_mongo:on_query(InstanceId, Request, ConnectorState). diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_rocketmq.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_rocketmq.erl new file mode 100644 index 000000000..84f2e2a89 --- /dev/null +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_rocketmq.erl @@ -0,0 +1,338 @@ +%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_ee_connector_rocketmq). + +-behaviour(emqx_resource). + +-include_lib("emqx_resource/include/emqx_resource.hrl"). +-include_lib("typerefl/include/types.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). + +-export([roots/0, fields/1]). + +%% `emqx_resource' API +-export([ + callback_mode/0, + is_buffer_supported/0, + on_start/2, + on_stop/2, + on_query/3, + on_batch_query/3, + on_get_status/2 +]). + +-import(hoconsc, [mk/2, enum/1, ref/2]). + +-define(ROCKETMQ_HOST_OPTIONS, #{ + default_port => 9876 +}). + +%%===================================================================== +%% Hocon schema +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]. + +fields(config) -> + [ + {server, server()}, + {topic, + mk( + binary(), + #{default => <<"TopicTest">>, desc => ?DESC(topic)} + )}, + {refresh_interval, + mk( + emqx_schema:duration(), + #{default => <<"3s">>, desc => ?DESC(refresh_interval)} + )}, + {send_buffer, + mk( + emqx_schema:bytesize(), + #{default => <<"1024KB">>, desc => ?DESC(send_buffer)} + )}, + {security_token, mk(binary(), #{default => <<>>, desc => ?DESC(security_token)})} + | relational_fields() + ]. + +add_default_username(Fields) -> + lists:map( + fun + ({username, OrigUsernameFn}) -> + {username, add_default_fn(OrigUsernameFn, <<"">>)}; + (Field) -> + Field + end, + Fields + ). + +add_default_fn(OrigFn, Default) -> + fun + (default) -> Default; + (Field) -> OrigFn(Field) + end. + +server() -> + Meta = #{desc => ?DESC("server")}, + emqx_schema:servers_sc(Meta, ?ROCKETMQ_HOST_OPTIONS). + +relational_fields() -> + Fields = [username, password, auto_reconnect], + Values = lists:filter( + fun({E, _}) -> lists:member(E, Fields) end, + emqx_connector_schema_lib:relational_db_fields() + ), + add_default_username(Values). + +%%======================================================================================== +%% `emqx_resource' API +%%======================================================================================== + +callback_mode() -> always_sync. + +is_buffer_supported() -> false. + +on_start( + InstanceId, + #{server := Server, topic := Topic} = Config1 +) -> + ?SLOG(info, #{ + msg => "starting_rocketmq_connector", + connector => InstanceId, + config => redact(Config1) + }), + Config = maps:merge(default_security_info(), Config1), + {Host, Port} = emqx_schema:parse_server(Server, ?ROCKETMQ_HOST_OPTIONS), + + Server1 = [{Host, Port}], + ClientId = client_id(InstanceId), + ClientCfg = #{acl_info => #{}}, + + TopicTks = emqx_plugin_libs_rule:preproc_tmpl(Topic), + ProducerOpts = make_producer_opts(Config), + Templates = parse_template(Config), + ProducersMapPID = create_producers_map(ClientId), + State = #{ + client_id => ClientId, + topic_tokens => TopicTks, + config => Config, + templates => Templates, + producers_map_pid => ProducersMapPID, + producers_opts => ProducerOpts + }, + + case rocketmq:ensure_supervised_client(ClientId, Server1, ClientCfg) of + {ok, _Pid} -> + {ok, State}; + {error, _Reason} = Error -> + ?tp( + rocketmq_connector_start_failed, + #{error => _Reason} + ), + Error + end. + +on_stop(InstanceId, #{client_id := ClientId, producers_map_pid := Pid} = _State) -> + ?SLOG(info, #{ + msg => "stopping_rocketmq_connector", + connector => InstanceId + }), + Pid ! ok, + ok = rocketmq:stop_and_delete_supervised_client(ClientId). + +on_query(InstanceId, Query, State) -> + do_query(InstanceId, Query, send_sync, State). + +%% We only support batch inserts and all messages must have the same topic +on_batch_query(InstanceId, [{send_message, _Msg} | _] = Query, State) -> + do_query(InstanceId, Query, batch_send_sync, State); +on_batch_query(_InstanceId, Query, _State) -> + {error, {unrecoverable_error, {invalid_request, Query}}}. + +on_get_status(_InstanceId, #{client_id := ClientId}) -> + case rocketmq_client_sup:find_client(ClientId) of + {ok, _Pid} -> + connected; + _ -> + connecting + end. + +%%======================================================================================== +%% Helper fns +%%======================================================================================== + +do_query( + InstanceId, + Query, + QueryFunc, + #{ + templates := Templates, + client_id := ClientId, + topic_tokens := TopicTks, + producers_opts := ProducerOpts, + config := #{topic := RawTopic, resource_opts := #{request_timeout := RequestTimeout}} + } = State +) -> + ?TRACE( + "QUERY", + "rocketmq_connector_received", + #{connector => InstanceId, query => Query, state => State} + ), + + TopicKey = get_topic_key(Query, RawTopic, TopicTks), + Data = apply_template(Query, Templates), + + Result = safe_do_produce( + InstanceId, QueryFunc, ClientId, TopicKey, Data, ProducerOpts, RequestTimeout + ), + case Result of + {error, Reason} -> + ?tp( + rocketmq_connector_query_return, + #{error => Reason} + ), + ?SLOG(error, #{ + msg => "rocketmq_connector_do_query_failed", + connector => InstanceId, + query => Query, + reason => Reason + }), + Result; + _ -> + ?tp( + rocketmq_connector_query_return, + #{result => Result} + ), + Result + end. + +safe_do_produce(InstanceId, QueryFunc, ClientId, TopicKey, Data, ProducerOpts, RequestTimeout) -> + try + Producers = get_producers(ClientId, TopicKey, ProducerOpts), + produce(InstanceId, QueryFunc, Producers, Data, RequestTimeout) + catch + _Type:Reason -> + {error, {unrecoverable_error, Reason}} + end. + +produce(_InstanceId, QueryFunc, Producers, Data, RequestTimeout) -> + rocketmq:QueryFunc(Producers, Data, RequestTimeout). + +parse_template(Config) -> + Templates = + case maps:get(template, Config, undefined) of + undefined -> #{}; + <<>> -> #{}; + Template -> #{send_message => Template} + end, + + parse_template(maps:to_list(Templates), #{}). + +parse_template([{Key, H} | T], Templates) -> + ParamsTks = emqx_plugin_libs_rule:preproc_tmpl(H), + parse_template( + T, + Templates#{Key => ParamsTks} + ); +parse_template([], Templates) -> + Templates. + +get_topic_key({_, Msg}, RawTopic, TopicTks) -> + {RawTopic, emqx_plugin_libs_rule:proc_tmpl(TopicTks, Msg)}; +get_topic_key([Query | _], RawTopic, TopicTks) -> + get_topic_key(Query, RawTopic, TopicTks). + +apply_template({Key, Msg} = _Req, Templates) -> + case maps:get(Key, Templates, undefined) of + undefined -> + emqx_json:encode(Msg); + Template -> + emqx_plugin_libs_rule:proc_tmpl(Template, Msg) + end; +apply_template([{Key, _} | _] = Reqs, Templates) -> + case maps:get(Key, Templates, undefined) of + undefined -> + [emqx_json:encode(Msg) || {_, Msg} <- Reqs]; + Template -> + [emqx_plugin_libs_rule:proc_tmpl(Template, Msg) || {_, Msg} <- Reqs] + end. + +client_id(InstanceId) -> + Name = emqx_resource_manager:manager_id_to_resource_id(InstanceId), + erlang:binary_to_atom(Name, utf8). + +redact(Msg) -> + emqx_misc:redact(Msg, fun is_sensitive_key/1). + +is_sensitive_key(security_token) -> + true; +is_sensitive_key(_) -> + false. + +make_producer_opts( + #{ + username := Username, + password := Password, + security_token := SecurityToken, + send_buffer := SendBuff, + refresh_interval := RefreshInterval + } +) -> + ACLInfo = acl_info(Username, Password, SecurityToken), + #{ + tcp_opts => [{sndbuf, SendBuff}], + ref_topic_route_interval => RefreshInterval, + acl_info => ACLInfo + }. + +acl_info(<<>>, <<>>, <<>>) -> + #{}; +acl_info(Username, Password, <<>>) when is_binary(Username), is_binary(Password) -> + #{ + access_key => Username, + secret_key => Password + }; +acl_info(Username, Password, SecurityToken) when + is_binary(Username), is_binary(Password), is_binary(SecurityToken) +-> + #{ + access_key => Username, + secret_key => Password, + security_token => SecurityToken + }; +acl_info(_, _, _) -> + #{}. + +create_producers_map(ClientId) -> + erlang:spawn(fun() -> + case ets:whereis(ClientId) of + undefined -> + _ = ets:new(ClientId, [public, named_table]), + ok; + _ -> + ok + end, + receive + _Msg -> + ok + end + end). + +get_producers(ClientId, {_, Topic1} = TopicKey, ProducerOpts) -> + case ets:lookup(ClientId, TopicKey) of + [{_, Producers0}] -> + Producers0; + _ -> + ProducerGroup = iolist_to_binary([atom_to_list(ClientId), "_", Topic1]), + {ok, Producers0} = rocketmq:ensure_supervised_producers( + ClientId, ProducerGroup, Topic1, ProducerOpts + ), + ets:insert(ClientId, {TopicKey, Producers0}), + Producers0 + end. + +default_security_info() -> + #{username => <<>>, password => <<>>, security_token => <<>>}. diff --git a/lib-ee/emqx_ee_connector/test/emqx_ee_connector_cassa_SUITE.erl b/lib-ee/emqx_ee_connector/test/emqx_ee_connector_cassa_SUITE.erl new file mode 100644 index 000000000..95b4407cf --- /dev/null +++ b/lib-ee/emqx_ee_connector/test/emqx_ee_connector_cassa_SUITE.erl @@ -0,0 +1,200 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_ee_connector_cassa_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include("emqx_connector.hrl"). +-include("emqx_ee_connector.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +%% Cassandra server defined at `.ci/docker-compose-file/docker-compose-cassandra-tcp.yaml` +%% You can change it to `127.0.0.1`, if you run this SUITE locally +-define(CASSANDRA_HOST, "cassandra"). +-define(CASSANDRA_RESOURCE_MOD, emqx_ee_connector_cassa). + +%% This test SUITE requires a running cassandra instance. If you don't want to +%% bring up the whole CI infrastuctucture with the `scripts/ct/run.sh` script +%% you can create a cassandra instance with the following command (execute it +%% from root of the EMQX directory.). You also need to set ?CASSANDRA_HOST and +%% ?CASSANDRA_PORT to appropriate values. +%% +%% sudo docker run --rm -d --name cassandra --network host cassandra:3.11.14 + +%% Cassandra default username & password once enable `authenticator: PasswordAuthenticator` +%% in cassandra config +-define(CASSA_USERNAME, <<"cassandra">>). +-define(CASSA_PASSWORD, <<"cassandra">>). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +groups() -> + []. + +cassandra_servers() -> + emqx_schema:parse_servers( + iolist_to_binary([?CASSANDRA_HOST, ":", erlang:integer_to_list(?CASSANDRA_DEFAULT_PORT)]), + #{default_port => ?CASSANDRA_DEFAULT_PORT} + ). + +init_per_suite(Config) -> + case + emqx_common_test_helpers:is_tcp_server_available(?CASSANDRA_HOST, ?CASSANDRA_DEFAULT_PORT) + of + true -> + ok = emqx_common_test_helpers:start_apps([emqx_conf]), + ok = emqx_connector_test_helpers:start_apps([emqx_resource]), + {ok, _} = application:ensure_all_started(emqx_connector), + {ok, _} = application:ensure_all_started(emqx_ee_connector), + %% keyspace `mqtt` must be created in advance + {ok, Conn} = + ecql:connect([ + {nodes, cassandra_servers()}, + {username, ?CASSA_USERNAME}, + {password, ?CASSA_PASSWORD}, + {keyspace, "mqtt"} + ]), + ecql:close(Conn), + Config; + false -> + case os:getenv("IS_CI") of + "yes" -> + throw(no_cassandra); + _ -> + {skip, no_cassandra} + end + end. + +end_per_suite(_Config) -> + ok = emqx_common_test_helpers:stop_apps([emqx_conf]), + ok = emqx_connector_test_helpers:stop_apps([emqx_resource]), + _ = application:stop(emqx_connector), + _ = application:stop(emqx_ee_connector). + +init_per_testcase(_, Config) -> + Config. + +end_per_testcase(_, _Config) -> + ok. + +%%-------------------------------------------------------------------- +%% cases +%%-------------------------------------------------------------------- + +t_lifecycle(_Config) -> + perform_lifecycle_check( + <<"emqx_connector_cassandra_SUITE">>, + cassandra_config() + ). + +show(X) -> + erlang:display(X), + X. + +show(Label, What) -> + erlang:display({Label, What}), + What. + +perform_lifecycle_check(PoolName, InitialConfig) -> + {ok, #{config := CheckedConfig}} = + emqx_resource:check_config(?CASSANDRA_RESOURCE_MOD, InitialConfig), + {ok, #{ + state := #{poolname := ReturnedPoolName} = State, + status := InitialStatus + }} = + emqx_resource:create_local( + PoolName, + ?CONNECTOR_RESOURCE_GROUP, + ?CASSANDRA_RESOURCE_MOD, + CheckedConfig, + #{} + ), + ?assertEqual(InitialStatus, connected), + % Instance should match the state and status of the just started resource + {ok, ?CONNECTOR_RESOURCE_GROUP, #{ + state := State, + status := InitialStatus + }} = + emqx_resource:get_instance(PoolName), + ?assertEqual({ok, connected}, emqx_resource:health_check(PoolName)), + % % Perform query as further check that the resource is working as expected + (fun() -> + erlang:display({pool_name, PoolName}), + QueryNoParamsResWrapper = emqx_resource:query(PoolName, test_query_no_params()), + ?assertMatch({ok, _}, QueryNoParamsResWrapper) + end)(), + ?assertEqual(ok, emqx_resource:stop(PoolName)), + % Resource will be listed still, but state will be changed and healthcheck will fail + % as the worker no longer exists. + {ok, ?CONNECTOR_RESOURCE_GROUP, #{ + state := State, + status := StoppedStatus + }} = + emqx_resource:get_instance(PoolName), + ?assertEqual(stopped, StoppedStatus), + ?assertEqual({error, resource_is_stopped}, emqx_resource:health_check(PoolName)), + % Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself. + ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), + % Can call stop/1 again on an already stopped instance + ?assertEqual(ok, emqx_resource:stop(PoolName)), + % Make sure it can be restarted and the healthchecks and queries work properly + ?assertEqual(ok, emqx_resource:restart(PoolName)), + % async restart, need to wait resource + timer:sleep(500), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} = + emqx_resource:get_instance(PoolName), + ?assertEqual({ok, connected}, emqx_resource:health_check(PoolName)), + (fun() -> + QueryNoParamsResWrapper = + emqx_resource:query(PoolName, test_query_no_params()), + ?assertMatch({ok, _}, QueryNoParamsResWrapper) + end)(), + % Stop and remove the resource in one go. + ?assertEqual(ok, emqx_resource:remove_local(PoolName)), + ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), + % Should not even be able to get the resource data out of ets now unlike just stopping. + ?assertEqual({error, not_found}, emqx_resource:get_instance(PoolName)). + +%%-------------------------------------------------------------------- +%% utils +%%-------------------------------------------------------------------- + +cassandra_config() -> + Config = + #{ + auto_reconnect => true, + keyspace => <<"mqtt">>, + username => ?CASSA_USERNAME, + password => ?CASSA_PASSWORD, + pool_size => 8, + servers => iolist_to_binary( + io_lib:format( + "~s:~b", + [ + ?CASSANDRA_HOST, + ?CASSANDRA_DEFAULT_PORT + ] + ) + ) + }, + #{<<"config">> => Config}. + +test_query_no_params() -> + {query, <<"SELECT count(1) AS T FROM system.local">>}. diff --git a/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl b/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl index 72fc11a67..364821ea0 100644 --- a/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl @@ -94,7 +94,7 @@ perform_lifecycle_check(PoolName, InitialConfig) -> emqx_resource:get_instance(PoolName), ?assertEqual({ok, connected}, emqx_resource:health_check(PoolName)), % % Perform query as further check that the resource is working as expected - ?assertMatch(ok, emqx_resource:query(PoolName, test_query())), + ?assertMatch({ok, 204, _}, emqx_resource:query(PoolName, test_query())), ?assertEqual(ok, emqx_resource:stop(PoolName)), % Resource will be listed still, but state will be changed and healthcheck will fail % as the worker no longer exists. @@ -116,7 +116,7 @@ perform_lifecycle_check(PoolName, InitialConfig) -> {ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} = emqx_resource:get_instance(PoolName), ?assertEqual({ok, connected}, emqx_resource:health_check(PoolName)), - ?assertMatch(ok, emqx_resource:query(PoolName, test_query())), + ?assertMatch({ok, 204, _}, emqx_resource:query(PoolName, test_query())), % Stop and remove the resource in one go. ?assertEqual(ok, emqx_resource:remove_local(PoolName)), ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), diff --git a/lib-ee/emqx_license/src/emqx_license.app.src b/lib-ee/emqx_license/src/emqx_license.app.src index 7a569c402..0a97ee83b 100644 --- a/lib-ee/emqx_license/src/emqx_license.app.src +++ b/lib-ee/emqx_license/src/emqx_license.app.src @@ -1,6 +1,6 @@ {application, emqx_license, [ {description, "EMQX License"}, - {vsn, "5.0.7"}, + {vsn, "5.0.8"}, {modules, []}, {registered, [emqx_license_sup]}, {applications, [kernel, stdlib, emqx_ctl]}, diff --git a/lib-ee/emqx_license/src/emqx_license_installer.erl b/lib-ee/emqx_license/src/emqx_license_installer.erl index 7a6ea5339..58ee6ebcc 100644 --- a/lib-ee/emqx_license/src/emqx_license_installer.erl +++ b/lib-ee/emqx_license/src/emqx_license_installer.erl @@ -74,13 +74,13 @@ ensure_timer(#{interval := Interval} = State) -> check_pid(#{name := Name, pid := OldPid, callback := Callback} = State) -> case whereis(Name) of undefined -> - ?tp(debug, emqx_license_installer_noproc, #{pid => OldPid}), + ?tp(debug, emqx_license_installer_noproc, #{old_pid => OldPid}), State; OldPid -> - ?tp(debug, emqx_license_installer_nochange, #{pid => OldPid}), + ?tp(debug, emqx_license_installer_nochange, #{old_pid => OldPid}), State; NewPid -> _ = Callback(), - ?tp(debug, emqx_license_installer_called, #{pid => OldPid}), + ?tp(debug, emqx_license_installer_called, #{old_pid => OldPid}), State#{pid => NewPid} end. diff --git a/mix.exs b/mix.exs index 6d76e23af..514c9139d 100644 --- a/mix.exs +++ b/mix.exs @@ -31,16 +31,17 @@ defmodule EMQXUmbrella.MixProject do def project() do profile_info = check_profile!() + version = pkg_vsn() [ app: :emqx_mix, - version: pkg_vsn(), - deps: deps(profile_info), + version: version, + deps: deps(profile_info, version), releases: releases() ] end - defp deps(profile_info) do + defp deps(profile_info, version) do # we need several overrides here because dependencies specify # other exact versions, and not ranges. [ @@ -52,16 +53,18 @@ defmodule EMQXUmbrella.MixProject do {:gproc, github: "uwiger/gproc", tag: "0.8.0", override: true}, {:jiffy, github: "emqx/jiffy", tag: "1.0.5", override: true}, {:cowboy, github: "emqx/cowboy", tag: "2.9.0", override: true}, - {:esockd, github: "emqx/esockd", tag: "5.9.4", override: true}, + {:esockd, github: "emqx/esockd", tag: "5.9.6", override: true}, {:rocksdb, github: "emqx/erlang-rocksdb", tag: "1.7.2-emqx-9", override: true}, - {:ekka, github: "emqx/ekka", tag: "0.14.5", override: true}, + {:ekka, github: "emqx/ekka", tag: "0.14.6", override: true}, {:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true}, {:grpc, github: "emqx/grpc-erl", tag: "0.6.7", override: true}, {:minirest, github: "emqx/minirest", tag: "1.3.8", override: true}, {:ecpool, github: "emqx/ecpool", tag: "0.5.3", override: true}, {:replayq, github: "emqx/replayq", tag: "0.3.7", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, - {:emqtt, github: "emqx/emqtt", tag: "1.8.5", override: true}, + # maybe forbid to fetch quicer + {:emqtt, + github: "emqx/emqtt", tag: "1.8.5", override: true, system_env: maybe_no_quic_env()}, {:rulesql, github: "emqx/rulesql", tag: "0.1.4"}, {:observer_cli, "1.7.1"}, {:system_monitor, github: "ieQu1/system_monitor", tag: "3.0.3"}, @@ -69,7 +72,7 @@ defmodule EMQXUmbrella.MixProject do # in conflict by emqtt and hocon {:getopt, "1.0.2", override: true}, {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "1.0.7", override: true}, - {:hocon, github: "emqx/hocon", tag: "0.37.2", override: true}, + {:hocon, github: "emqx/hocon", tag: "0.38.0", override: true}, {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.2", override: true}, {:esasl, github: "emqx/esasl", tag: "0.2.0"}, {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"}, @@ -92,11 +95,15 @@ defmodule EMQXUmbrella.MixProject do {:gpb, "4.19.5", override: true, runtime: false}, {:hackney, github: "benoitc/hackney", tag: "1.18.1", override: true} ] ++ - umbrella_apps() ++ - enterprise_apps(profile_info) ++ + emqx_apps(profile_info, version) ++ enterprise_deps(profile_info) ++ bcrypt_dep() ++ jq_dep() ++ quicer_dep() end + defp emqx_apps(profile_info, version) do + apps = umbrella_apps() ++ enterprise_apps(profile_info) + set_emqx_app_system_env(apps, profile_info, version) + end + defp umbrella_apps() do "apps/*" |> Path.wildcard() @@ -145,6 +152,46 @@ defmodule EMQXUmbrella.MixProject do [] end + defp set_emqx_app_system_env(apps, profile_info, version) do + system_env = emqx_app_system_env(profile_info, version) ++ maybe_no_quic_env() + + Enum.map( + apps, + fn {app, opts} -> + {app, + Keyword.update( + opts, + :system_env, + system_env, + &Keyword.merge(&1, system_env) + )} + end + ) + end + + def emqx_app_system_env(profile_info, version) do + erlc_options(profile_info, version) + |> dump_as_erl() + |> then(&[{"ERL_COMPILER_OPTIONS", &1}]) + end + + defp erlc_options(%{edition_type: edition_type}, version) do + [ + :debug_info, + {:compile_info, [{:emqx_vsn, String.to_charlist(version)}]}, + {:d, :EMQX_RELEASE_EDITION, erlang_edition(edition_type)}, + {:d, :snk_kind, :msg} + ] + end + + def maybe_no_quic_env() do + if not enable_quicer?() do + [{"BUILD_WITHOUT_QUIC", "true"}] + else + [] + end + end + defp releases() do [ emqx: fn -> @@ -347,10 +394,12 @@ defmodule EMQXUmbrella.MixProject do bin = Path.join(release.path, "bin") etc = Path.join(release.path, "etc") log = Path.join(release.path, "log") + plugins = Path.join(release.path, "plugins") Mix.Generator.create_directory(bin) Mix.Generator.create_directory(etc) Mix.Generator.create_directory(log) + Mix.Generator.create_directory(plugins) Mix.Generator.create_directory(Path.join(etc, "certs")) Enum.each( @@ -563,6 +612,7 @@ defmodule EMQXUmbrella.MixProject do &[ "etc", "data", + "plugins", "bin/node_dump" | &1 ] @@ -651,7 +701,7 @@ defmodule EMQXUmbrella.MixProject do defp quicer_dep() do if enable_quicer?(), # in conflict with emqx and emqtt - do: [{:quicer, github: "emqx/quic", tag: "0.0.113", override: true}], + do: [{:quicer, github: "emqx/quic", tag: "0.0.114", override: true}], else: [] end @@ -804,4 +854,13 @@ defmodule EMQXUmbrella.MixProject do |> List.first() end end + + defp dump_as_erl(term) do + term + |> then(&:io_lib.format("~0p", [&1])) + |> :erlang.iolist_to_binary() + end + + defp erlang_edition(:community), do: :ce + defp erlang_edition(:enterprise), do: :ee end diff --git a/rebar.config b/rebar.config index 4ef9852b4..b641077ea 100644 --- a/rebar.config +++ b/rebar.config @@ -37,7 +37,13 @@ {cover_opts, [verbose]}. {cover_export_enabled, true}. -{cover_excl_mods, [emqx_exproto_pb, emqx_exhook_pb]}. +{cover_excl_mods, + [ %% generated protobuf modules + emqx_exproto_pb, + emqx_exhook_pb, + %% taken almost as-is from OTP + emqx_ssl_crl_cache + ]}. {provider_hooks, [{pre, [{release, {relup_helper, gen_appups}}]}]}. @@ -54,9 +60,9 @@ , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}} - , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}} + , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.6"}}} , {rocksdb, {git, "https://github.com/emqx/erlang-rocksdb", {tag, "1.7.2-emqx-9"}}} - , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.5"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.6"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}} , {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.7"}}} , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.8"}}} @@ -69,7 +75,7 @@ , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}} , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.7"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.37.2"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.38.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}} diff --git a/rebar.config.erl b/rebar.config.erl index e976d7729..98cd30570 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -39,7 +39,7 @@ bcrypt() -> {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {tag, "0.6.0"}}}. quicer() -> - {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.113"}}}. + {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.114"}}}. jq() -> {jq, {git, "https://github.com/emqx/jq", {tag, "v0.3.9"}}}. diff --git a/rel/emqx_conf.template.en.md b/rel/emqx_conf.template.en.md index bbeac9489..8740e4319 100644 --- a/rel/emqx_conf.template.en.md +++ b/rel/emqx_conf.template.en.md @@ -4,13 +4,12 @@ and a superset of JSON. ## Layered -EMQX configuration consists of 3 layers. +EMQX configuration consists of two layers. From bottom up: 1. Immutable base: `emqx.conf` + `EMQX_` prefixed environment variables.
Changes in this layer require a full node restart to take effect. 1. Cluster overrides: `$EMQX_NODE__DATA_DIR/configs/cluster-override.conf` -1. Local node overrides: `$EMQX_NODE__DATA_DIR/configs/local-override.conf` When environment variable `$EMQX_NODE__DATA_DIR` is not set, config `node.data_dir` is used. diff --git a/rel/emqx_conf.template.zh.md b/rel/emqx_conf.template.zh.md index cfb620c0f..9402760a2 100644 --- a/rel/emqx_conf.template.zh.md +++ b/rel/emqx_conf.template.zh.md @@ -3,12 +3,11 @@ HOCON(Human-Optimized Config Object Notation)是一个JSON的超集,非常 ## 分层结构 -EMQX的配置文件可分为三层,自底向上依次是: +EMQX的配置文件可分为二层,自底向上依次是: 1. 不可变的基础层 `emqx.conf` 加上 `EMQX_` 前缀的环境变量。
修改这一层的配置之后,需要重启节点来使之生效。 1. 集群范围重载层:`$EMQX_NODE__DATA_DIR/configs/cluster-override.conf` -1. 节点本地重载层:`$EMQX_NODE__DATA_DIR/configs/local-override.conf` 如果环境变量 `$EMQX_NODE__DATA_DIR` 没有设置,那么该目录会从 `emqx.conf` 的 `node.data_dir` 配置中读取。 diff --git a/apps/emqx_authn/i18n/emqx_authn_api_i18n.conf b/rel/i18n/emqx_authn_api.hocon similarity index 100% rename from apps/emqx_authn/i18n/emqx_authn_api_i18n.conf rename to rel/i18n/emqx_authn_api.hocon diff --git a/apps/emqx_authn/i18n/emqx_authn_http_i18n.conf b/rel/i18n/emqx_authn_http.hocon similarity index 100% rename from apps/emqx_authn/i18n/emqx_authn_http_i18n.conf rename to rel/i18n/emqx_authn_http.hocon diff --git a/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf b/rel/i18n/emqx_authn_jwt.hocon similarity index 100% rename from apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf rename to rel/i18n/emqx_authn_jwt.hocon diff --git a/apps/emqx_authn/i18n/emqx_authn_mnesia_i18n.conf b/rel/i18n/emqx_authn_mnesia.hocon similarity index 100% rename from apps/emqx_authn/i18n/emqx_authn_mnesia_i18n.conf rename to rel/i18n/emqx_authn_mnesia.hocon diff --git a/apps/emqx_authn/i18n/emqx_authn_mongodb_i18n.conf b/rel/i18n/emqx_authn_mongodb.hocon similarity index 100% rename from apps/emqx_authn/i18n/emqx_authn_mongodb_i18n.conf rename to rel/i18n/emqx_authn_mongodb.hocon diff --git a/apps/emqx_authn/i18n/emqx_authn_mysql_i18n.conf b/rel/i18n/emqx_authn_mysql.hocon similarity index 100% rename from apps/emqx_authn/i18n/emqx_authn_mysql_i18n.conf rename to rel/i18n/emqx_authn_mysql.hocon diff --git a/apps/emqx_authn/i18n/emqx_authn_pgsql_i18n.conf b/rel/i18n/emqx_authn_pgsql.hocon similarity index 100% rename from apps/emqx_authn/i18n/emqx_authn_pgsql_i18n.conf rename to rel/i18n/emqx_authn_pgsql.hocon diff --git a/apps/emqx_authn/i18n/emqx_authn_redis_i18n.conf b/rel/i18n/emqx_authn_redis.hocon similarity index 100% rename from apps/emqx_authn/i18n/emqx_authn_redis_i18n.conf rename to rel/i18n/emqx_authn_redis.hocon diff --git a/apps/emqx_authn/i18n/emqx_authn_schema_i18n.conf b/rel/i18n/emqx_authn_schema.hocon similarity index 100% rename from apps/emqx_authn/i18n/emqx_authn_schema_i18n.conf rename to rel/i18n/emqx_authn_schema.hocon diff --git a/apps/emqx_authn/i18n/emqx_authn_user_import_api_i18n.conf b/rel/i18n/emqx_authn_user_import_api.hocon similarity index 100% rename from apps/emqx_authn/i18n/emqx_authn_user_import_api_i18n.conf rename to rel/i18n/emqx_authn_user_import_api.hocon diff --git a/apps/emqx_authz/i18n/emqx_authz_api_cache_i18n.conf b/rel/i18n/emqx_authz_api_cache.hocon similarity index 100% rename from apps/emqx_authz/i18n/emqx_authz_api_cache_i18n.conf rename to rel/i18n/emqx_authz_api_cache.hocon diff --git a/apps/emqx_authz/i18n/emqx_authz_api_mnesia_i18n.conf b/rel/i18n/emqx_authz_api_mnesia.hocon similarity index 100% rename from apps/emqx_authz/i18n/emqx_authz_api_mnesia_i18n.conf rename to rel/i18n/emqx_authz_api_mnesia.hocon diff --git a/apps/emqx_authz/i18n/emqx_authz_api_schema_i18n.conf b/rel/i18n/emqx_authz_api_schema.hocon similarity index 100% rename from apps/emqx_authz/i18n/emqx_authz_api_schema_i18n.conf rename to rel/i18n/emqx_authz_api_schema.hocon diff --git a/apps/emqx_authz/i18n/emqx_authz_api_settings_i18n.conf b/rel/i18n/emqx_authz_api_settings.hocon similarity index 100% rename from apps/emqx_authz/i18n/emqx_authz_api_settings_i18n.conf rename to rel/i18n/emqx_authz_api_settings.hocon diff --git a/apps/emqx_authz/i18n/emqx_authz_api_sources_i18n.conf b/rel/i18n/emqx_authz_api_sources.hocon similarity index 100% rename from apps/emqx_authz/i18n/emqx_authz_api_sources_i18n.conf rename to rel/i18n/emqx_authz_api_sources.hocon diff --git a/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf b/rel/i18n/emqx_authz_schema.hocon similarity index 100% rename from apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf rename to rel/i18n/emqx_authz_schema.hocon diff --git a/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_api_i18n.conf b/rel/i18n/emqx_auto_subscribe_api.hocon similarity index 100% rename from apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_api_i18n.conf rename to rel/i18n/emqx_auto_subscribe_api.hocon diff --git a/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_i18n.conf b/rel/i18n/emqx_auto_subscribe_schema.hocon similarity index 100% rename from apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_i18n.conf rename to rel/i18n/emqx_auto_subscribe_schema.hocon diff --git a/apps/emqx_bridge/i18n/emqx_bridge_api.conf b/rel/i18n/emqx_bridge_api.hocon similarity index 100% rename from apps/emqx_bridge/i18n/emqx_bridge_api.conf rename to rel/i18n/emqx_bridge_api.hocon diff --git a/apps/emqx_bridge/i18n/emqx_bridge_mqtt_schema.conf b/rel/i18n/emqx_bridge_mqtt_schema.hocon similarity index 100% rename from apps/emqx_bridge/i18n/emqx_bridge_mqtt_schema.conf rename to rel/i18n/emqx_bridge_mqtt_schema.hocon diff --git a/apps/emqx_bridge/i18n/emqx_bridge_schema.conf b/rel/i18n/emqx_bridge_schema.hocon similarity index 96% rename from apps/emqx_bridge/i18n/emqx_bridge_schema.conf rename to rel/i18n/emqx_bridge_schema.hocon index 901f25455..de4ceb0d5 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_schema.conf +++ b/rel/i18n/emqx_bridge_schema.hocon @@ -54,6 +54,17 @@ emqx_bridge_schema { } } + desc_status_reason { + desc { + en: "This is the reason given in case a bridge is failing to connect." + zh: "桥接连接失败的原因。" + } + label: { + en: "Failure reason" + zh: "失败原因" + } + } + desc_node_status { desc { en: """The status of the bridge for each node. diff --git a/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf b/rel/i18n/emqx_bridge_webhook_schema.hocon similarity index 100% rename from apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf rename to rel/i18n/emqx_bridge_webhook_schema.hocon diff --git a/apps/emqx_gateway/i18n/emqx_coap_api_i18n.conf b/rel/i18n/emqx_coap_api.hocon similarity index 100% rename from apps/emqx_gateway/i18n/emqx_coap_api_i18n.conf rename to rel/i18n/emqx_coap_api.hocon diff --git a/apps/emqx_conf/i18n/emqx_conf_schema.conf b/rel/i18n/emqx_conf_schema.hocon similarity index 100% rename from apps/emqx_conf/i18n/emqx_conf_schema.conf rename to rel/i18n/emqx_conf_schema.hocon diff --git a/apps/emqx_connector/i18n/emqx_connector_api.conf b/rel/i18n/emqx_connector_api.hocon similarity index 100% rename from apps/emqx_connector/i18n/emqx_connector_api.conf rename to rel/i18n/emqx_connector_api.hocon diff --git a/apps/emqx_connector/i18n/emqx_connector_http.conf b/rel/i18n/emqx_connector_http.hocon similarity index 100% rename from apps/emqx_connector/i18n/emqx_connector_http.conf rename to rel/i18n/emqx_connector_http.hocon diff --git a/apps/emqx_connector/i18n/emqx_connector_ldap.conf b/rel/i18n/emqx_connector_ldap.hocon similarity index 100% rename from apps/emqx_connector/i18n/emqx_connector_ldap.conf rename to rel/i18n/emqx_connector_ldap.hocon diff --git a/apps/emqx_connector/i18n/emqx_connector_mongo.conf b/rel/i18n/emqx_connector_mongo.hocon similarity index 100% rename from apps/emqx_connector/i18n/emqx_connector_mongo.conf rename to rel/i18n/emqx_connector_mongo.hocon diff --git a/apps/emqx_connector/i18n/emqx_connector_mqtt.conf b/rel/i18n/emqx_connector_mqtt.hocon similarity index 100% rename from apps/emqx_connector/i18n/emqx_connector_mqtt.conf rename to rel/i18n/emqx_connector_mqtt.hocon diff --git a/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf b/rel/i18n/emqx_connector_mqtt_schema.hocon similarity index 100% rename from apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf rename to rel/i18n/emqx_connector_mqtt_schema.hocon diff --git a/apps/emqx_connector/i18n/emqx_connector_mysql.conf b/rel/i18n/emqx_connector_mysql.hocon similarity index 100% rename from apps/emqx_connector/i18n/emqx_connector_mysql.conf rename to rel/i18n/emqx_connector_mysql.hocon diff --git a/apps/emqx_connector/i18n/emqx_connector_pgsql.conf b/rel/i18n/emqx_connector_pgsql.hocon similarity index 100% rename from apps/emqx_connector/i18n/emqx_connector_pgsql.conf rename to rel/i18n/emqx_connector_pgsql.hocon diff --git a/apps/emqx_connector/i18n/emqx_connector_redis.conf b/rel/i18n/emqx_connector_redis.hocon similarity index 100% rename from apps/emqx_connector/i18n/emqx_connector_redis.conf rename to rel/i18n/emqx_connector_redis.hocon diff --git a/apps/emqx_connector/i18n/emqx_connector_schema_lib.conf b/rel/i18n/emqx_connector_schema_lib.hocon similarity index 100% rename from apps/emqx_connector/i18n/emqx_connector_schema_lib.conf rename to rel/i18n/emqx_connector_schema_lib.hocon diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_api_i18n.conf b/rel/i18n/emqx_dashboard_api.hocon similarity index 100% rename from apps/emqx_dashboard/i18n/emqx_dashboard_api_i18n.conf rename to rel/i18n/emqx_dashboard_api.hocon diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/rel/i18n/emqx_dashboard_schema.hocon similarity index 100% rename from apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf rename to rel/i18n/emqx_dashboard_schema.hocon diff --git a/apps/emqx_modules/i18n/emqx_delayed_api_i18n.conf b/rel/i18n/emqx_delayed_api.hocon similarity index 100% rename from apps/emqx_modules/i18n/emqx_delayed_api_i18n.conf rename to rel/i18n/emqx_delayed_api.hocon diff --git a/rel/i18n/emqx_ee_bridge_cassa.hocon b/rel/i18n/emqx_ee_bridge_cassa.hocon new file mode 100644 index 000000000..3bbac6658 --- /dev/null +++ b/rel/i18n/emqx_ee_bridge_cassa.hocon @@ -0,0 +1,72 @@ +emqx_ee_bridge_cassa { + + local_topic { + desc { + en: """The MQTT topic filter to be forwarded to Cassandra. All MQTT 'PUBLISH' messages with the topic +matching the local_topic will be forwarded.
+NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is +configured, then both the data got from the rule and the MQTT messages that match local_topic +will be forwarded.""" + zh: """发送到 'local_topic' 的消息都会转发到 Cassandra。
+注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。""" + } + label { + en: "Local Topic" + zh: "本地 Topic" + } + } + + cql_template { + desc { + en: """CQL Template""" + zh: """CQL 模板""" + } + label { + en: "CQL Template" + zh: "CQL 模板" + } + } + config_enable { + desc { + en: """Enable or disable this bridge""" + zh: """启用/禁用桥接""" + } + label { + en: "Enable Or Disable Bridge" + zh: "启用/禁用桥接" + } + } + + desc_config { + desc { + en: """Configuration for a Cassandra bridge.""" + zh: """Cassandra 桥接配置""" + } + label: { + en: "Cassandra Bridge Configuration" + zh: "Cassandra 桥接配置" + } + } + + desc_type { + desc { + en: """The Bridge Type""" + zh: """Bridge 类型""" + } + label { + en: "Bridge Type" + zh: "桥接类型" + } + } + + desc_name { + desc { + en: """Bridge name.""" + zh: """桥接名字""" + } + label { + en: "Bridge Name" + zh: "桥接名字" + } + } +} diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_clickhouse.conf b/rel/i18n/emqx_ee_bridge_clickhouse.hocon similarity index 98% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_clickhouse.conf rename to rel/i18n/emqx_ee_bridge_clickhouse.hocon index 4cfb4df00..b54f4dc70 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_clickhouse.conf +++ b/rel/i18n/emqx_ee_bridge_clickhouse.hocon @@ -6,11 +6,9 @@ emqx_ee_bridge_clickhouse { matching the local_topic will be forwarded.
NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is configured, then both the data got from the rule and the MQTT messages that match local_topic -will be forwarded. -""" +will be forwarded.""" zh: """发送到 'local_topic' 的消息都会转发到 Clickhouse。
-注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。 -""" +注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。""" } label { en: "Local Topic" diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_dynamo.conf b/rel/i18n/emqx_ee_bridge_dynamo.hocon similarity index 100% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_dynamo.conf rename to rel/i18n/emqx_ee_bridge_dynamo.hocon diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_gcp_pubsub.conf b/rel/i18n/emqx_ee_bridge_gcp_pubsub.hocon similarity index 100% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_gcp_pubsub.conf rename to rel/i18n/emqx_ee_bridge_gcp_pubsub.hocon diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_hstreamdb.conf b/rel/i18n/emqx_ee_bridge_hstreamdb.hocon similarity index 100% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_hstreamdb.conf rename to rel/i18n/emqx_ee_bridge_hstreamdb.hocon diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_influxdb.conf b/rel/i18n/emqx_ee_bridge_influxdb.hocon similarity index 100% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_influxdb.conf rename to rel/i18n/emqx_ee_bridge_influxdb.hocon diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf b/rel/i18n/emqx_ee_bridge_kafka.hocon similarity index 99% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf rename to rel/i18n/emqx_ee_bridge_kafka.hocon index df32c1cae..d1a017416 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf +++ b/rel/i18n/emqx_ee_bridge_kafka.hocon @@ -547,7 +547,7 @@ emqx_ee_bridge_kafka { "ts: message timestamp.\n" "ts_type: message timestamp type, which is one of" " create, append or undefined.\n" - "value: Kafka message value (uses the chosen value encoding).\n" + "value: Kafka message value (uses the chosen value encoding)." zh: "用于转换收到的 Kafka 消息的模板。 " "默认情况下,它将使用 JSON 格式来序列化来自 Kafka 的所有字段。 " "这些字段包括:" @@ -558,7 +558,7 @@ emqx_ee_bridge_kafka { "ts: 消息的时间戳。\n" "ts_type:消息的时间戳类型,值可能是:" " createappendundefined。\n" - "value: Kafka 消息值(使用选择的编码方式编码)。\n" + "value: Kafka 消息值(使用选择的编码方式编码)。" } label { diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf b/rel/i18n/emqx_ee_bridge_mongodb.hocon similarity index 100% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf rename to rel/i18n/emqx_ee_bridge_mongodb.hocon diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mysql.conf b/rel/i18n/emqx_ee_bridge_mysql.hocon similarity index 100% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mysql.conf rename to rel/i18n/emqx_ee_bridge_mysql.hocon diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_pgsql.conf b/rel/i18n/emqx_ee_bridge_pgsql.hocon similarity index 100% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_pgsql.conf rename to rel/i18n/emqx_ee_bridge_pgsql.hocon diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf b/rel/i18n/emqx_ee_bridge_redis.hocon similarity index 100% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf rename to rel/i18n/emqx_ee_bridge_redis.hocon diff --git a/rel/i18n/emqx_ee_bridge_rocketmq.hocon b/rel/i18n/emqx_ee_bridge_rocketmq.hocon new file mode 100644 index 000000000..2e33e6c07 --- /dev/null +++ b/rel/i18n/emqx_ee_bridge_rocketmq.hocon @@ -0,0 +1,70 @@ +emqx_ee_bridge_rocketmq { + + local_topic { + desc { + en: """The MQTT topic filter to be forwarded to RocketMQ. All MQTT `PUBLISH` messages with the topic +matching the `local_topic` will be forwarded.
+NOTE: if the bridge is used as a rule action, `local_topic` should be left empty otherwise the messages will be duplicated.""" + zh: """发送到 'local_topic' 的消息都会转发到 RocketMQ。
+注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。""" + } + label { + en: "Local Topic" + zh: "本地 Topic" + } + } + + template { + desc { + en: """Template, the default value is empty. When this value is empty the whole message will be stored in the RocketMQ""" + zh: """模板, 默认为空,为空时将会将整个消息转发给 RocketMQ""" + } + label { + en: "Template" + zh: "模板" + } + } + config_enable { + desc { + en: """Enable or disable this bridge""" + zh: """启用/禁用桥接""" + } + label { + en: "Enable Or Disable Bridge" + zh: "启用/禁用桥接" + } + } + + desc_config { + desc { + en: """Configuration for a RocketMQ bridge.""" + zh: """RocketMQ 桥接配置""" + } + label: { + en: "RocketMQ Bridge Configuration" + zh: "RocketMQ 桥接配置" + } + } + + desc_type { + desc { + en: """The Bridge Type""" + zh: """Bridge 类型""" + } + label { + en: "Bridge Type" + zh: "桥接类型" + } + } + + desc_name { + desc { + en: """Bridge name.""" + zh: """桥接名字""" + } + label { + en: "Bridge Name" + zh: "桥接名字" + } + } +} diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_tdengine.conf b/rel/i18n/emqx_ee_bridge_tdengine.hocon similarity index 96% rename from lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_tdengine.conf rename to rel/i18n/emqx_ee_bridge_tdengine.hocon index 2d5af9f16..21fc013df 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_tdengine.conf +++ b/rel/i18n/emqx_ee_bridge_tdengine.hocon @@ -6,11 +6,9 @@ emqx_ee_bridge_tdengine { matching the local_topic will be forwarded.
NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is configured, then both the data got from the rule and the MQTT messages that match local_topic -will be forwarded. -""" +will be forwarded.""" zh: """发送到 'local_topic' 的消息都会转发到 TDengine。
-注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。 -""" +注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。""" } label { en: "Local Topic" diff --git a/rel/i18n/emqx_ee_connector_cassa.hocon b/rel/i18n/emqx_ee_connector_cassa.hocon new file mode 100644 index 000000000..ecf004722 --- /dev/null +++ b/rel/i18n/emqx_ee_connector_cassa.hocon @@ -0,0 +1,28 @@ +emqx_ee_connector_cassa { + + servers { + desc { + en: """The IPv4 or IPv6 address or the hostname to connect to.
+A host entry has the following form: `Host[:Port][,Host2:Port]`.
+The Cassandra default port 9042 is used if `[:Port]` is not specified.""" + zh: """将要连接的 IPv4 或 IPv6 地址,或者主机名。
+主机名具有以下形式:`Host[:Port][,Host2:Port]`。
+如果未指定 `[:Port]`,则使用 Cassandra 默认端口 9042。""" + } + label: { + en: "Servers" + zh: "Servers" + } + } + + keyspace { + desc { + en: """Keyspace name to connect to.""" + zh: """要连接到的 Keyspace 名称。""" + } + label: { + en: "Keyspace" + zh: "Keyspace" + } + } +} diff --git a/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_clickhouse.conf b/rel/i18n/emqx_ee_connector_clickhouse.hocon similarity index 99% rename from lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_clickhouse.conf rename to rel/i18n/emqx_ee_connector_clickhouse.hocon index 069505a69..4d30e1715 100644 --- a/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_clickhouse.conf +++ b/rel/i18n/emqx_ee_connector_clickhouse.hocon @@ -1,4 +1,3 @@ - emqx_ee_connector_clickhouse { base_url { diff --git a/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_dynamo.conf b/rel/i18n/emqx_ee_connector_dynamo.hocon similarity index 63% rename from lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_dynamo.conf rename to rel/i18n/emqx_ee_connector_dynamo.hocon index e1fc11e03..295929a72 100644 --- a/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_dynamo.conf +++ b/rel/i18n/emqx_ee_connector_dynamo.hocon @@ -2,8 +2,8 @@ emqx_ee_connector_dynamo { url { desc { - en: """The url of DynamoDB endpoint.
""" - zh: """DynamoDB 的地址。
""" + en: """The url of DynamoDB endpoint.""" + zh: """DynamoDB 的地址。""" } label: { en: "DynamoDB Endpoint" diff --git a/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_hstreamdb.conf b/rel/i18n/emqx_ee_connector_hstreamdb.hocon similarity index 100% rename from lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_hstreamdb.conf rename to rel/i18n/emqx_ee_connector_hstreamdb.hocon diff --git a/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_influxdb.conf b/rel/i18n/emqx_ee_connector_influxdb.hocon similarity index 100% rename from lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_influxdb.conf rename to rel/i18n/emqx_ee_connector_influxdb.hocon diff --git a/rel/i18n/emqx_ee_connector_rocketmq.hocon b/rel/i18n/emqx_ee_connector_rocketmq.hocon new file mode 100644 index 000000000..44dda7931 --- /dev/null +++ b/rel/i18n/emqx_ee_connector_rocketmq.hocon @@ -0,0 +1,62 @@ +emqx_ee_connector_rocketmq { + + server { + desc { + en: """The IPv4 or IPv6 address or the hostname to connect to.
+A host entry has the following form: `Host[:Port]`.
+The RocketMQ default port 9876 is used if `[:Port]` is not specified.""" + zh: """将要连接的 IPv4 或 IPv6 地址,或者主机名。
+主机名具有以下形式:`Host[:Port]`。
+如果未指定 `[:Port]`,则使用 RocketMQ 默认端口 9876。""" + } + label: { + en: "Server Host" + zh: "服务器地址" + } + } + + topic { + desc { + en: """RocketMQ Topic""" + zh: """RocketMQ 主题""" + } + label: { + en: "RocketMQ Topic" + zh: "RocketMQ 主题" + } + } + + refresh_interval { + desc { + en: """RocketMQ Topic Route Refresh Interval.""" + zh: """RocketMQ 主题路由更新间隔。""" + } + label: { + en: "Topic Route Refresh Interval" + zh: "主题路由更新间隔" + } + } + + send_buffer { + desc { + en: """The socket send buffer size of the RocketMQ driver client.""" + zh: """RocketMQ 驱动的套字节发送消息的缓冲区大小""" + } + label: { + en: "Send Buffer Size" + zh: "发送消息的缓冲区大小" + } + } + + security_token { + desc { + en: """RocketMQ Server Security Token""" + zh: """RocketMQ 服务器安全令牌""" + } + label: { + en: "Security Token" + zh: "安全令牌" + } + } + +} diff --git a/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_tdengine.conf b/rel/i18n/emqx_ee_connector_tdengine.hocon similarity index 69% rename from lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_tdengine.conf rename to rel/i18n/emqx_ee_connector_tdengine.hocon index c6c58d82d..02254124c 100644 --- a/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_tdengine.conf +++ b/rel/i18n/emqx_ee_connector_tdengine.hocon @@ -2,16 +2,12 @@ emqx_ee_connector_tdengine { server { desc { - en: """ -The IPv4 or IPv6 address or the hostname to connect to.
+ en: """The IPv4 or IPv6 address or the hostname to connect to.
A host entry has the following form: `Host[:Port]`.
-The TDengine default port 6041 is used if `[:Port]` is not specified. -""" - zh: """ -将要连接的 IPv4 或 IPv6 地址,或者主机名。
+The TDengine default port 6041 is used if `[:Port]` is not specified.""" + zh: """将要连接的 IPv4 或 IPv6 地址,或者主机名。
主机名具有以下形式:`Host[:Port]`。
-如果未指定 `[:Port]`,则使用 TDengine 默认端口 6041。 -""" +如果未指定 `[:Port]`,则使用 TDengine 默认端口 6041。""" } label: { en: "Server Host" diff --git a/apps/emqx_exhook/i18n/emqx_exhook_api_i18n.conf b/rel/i18n/emqx_exhook_api.hocon similarity index 95% rename from apps/emqx_exhook/i18n/emqx_exhook_api_i18n.conf rename to rel/i18n/emqx_exhook_api.hocon index 46854a3db..3ec5367ed 100644 --- a/apps/emqx_exhook/i18n/emqx_exhook_api_i18n.conf +++ b/rel/i18n/emqx_exhook_api.hocon @@ -49,6 +49,10 @@ NOTE: The position should be \"front | rear | before:{name} | after:{name}""" zh: """移动 Exhook 服务器顺序。 注意: 移动的参数只能是:front | rear | before:{name} | after:{name}""" } + label { + en: "Change order of execution for registered Exhook server" + zh: "改变已注册的Exhook服务器的执行顺序" + } } move_position { diff --git a/apps/emqx_exhook/i18n/emqx_exhook_i18n.conf b/rel/i18n/emqx_exhook_schema.hocon similarity index 100% rename from apps/emqx_exhook/i18n/emqx_exhook_i18n.conf rename to rel/i18n/emqx_exhook_schema.hocon diff --git a/apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf b/rel/i18n/emqx_gateway_api.hocon similarity index 100% rename from apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf rename to rel/i18n/emqx_gateway_api.hocon diff --git a/apps/emqx_gateway/i18n/emqx_gateway_api_authn_i18n.conf b/rel/i18n/emqx_gateway_api_authn.hocon similarity index 100% rename from apps/emqx_gateway/i18n/emqx_gateway_api_authn_i18n.conf rename to rel/i18n/emqx_gateway_api_authn.hocon diff --git a/apps/emqx_gateway/i18n/emqx_gateway_api_clients_i18n.conf b/rel/i18n/emqx_gateway_api_clients.hocon similarity index 100% rename from apps/emqx_gateway/i18n/emqx_gateway_api_clients_i18n.conf rename to rel/i18n/emqx_gateway_api_clients.hocon diff --git a/apps/emqx_gateway/i18n/emqx_gateway_api_listeners_i18n.conf b/rel/i18n/emqx_gateway_api_listeners.hocon similarity index 100% rename from apps/emqx_gateway/i18n/emqx_gateway_api_listeners_i18n.conf rename to rel/i18n/emqx_gateway_api_listeners.hocon diff --git a/apps/emqx_gateway/i18n/emqx_gateway_schema_i18n.conf b/rel/i18n/emqx_gateway_schema.hocon similarity index 97% rename from apps/emqx_gateway/i18n/emqx_gateway_schema_i18n.conf rename to rel/i18n/emqx_gateway_schema.hocon index 74a70eb73..ebc955557 100644 --- a/apps/emqx_gateway/i18n/emqx_gateway_schema_i18n.conf +++ b/rel/i18n/emqx_gateway_schema.hocon @@ -370,13 +370,6 @@ After succeed observe a resource of LwM2M client, Gateway will send the notify e } } - gateway_common_mountpoint { - desc { - en: """""" - zh: """""" - } - } - gateway_common_clientinfo_override { desc { en: """ClientInfo override.""" @@ -431,10 +424,10 @@ After succeed observe a resource of LwM2M client, Gateway will send the notify e } } - tcp_listener { + listener_name_to_settings_map{ desc { - en: """""" - zh: """""" + en: """A map from listener names to listener settings.""" + zh: """从监听器名称到配置参数的映射。""" } } @@ -468,13 +461,6 @@ EMQX will close the TCP connection if proxy protocol packet is not received with } } - ssl_listener { - desc { - en: """""" - zh: """""" - } - } - ssl_listener_options { desc { en: """SSL Socket options.""" @@ -482,13 +468,6 @@ EMQX will close the TCP connection if proxy protocol packet is not received with } } - udp_listener { - desc { - en: """""" - zh: """""" - } - } - udp_listener_udp_opts { desc { en: """Settings for the UDP sockets.""" @@ -533,13 +512,6 @@ See: https://erlang.org/doc/man/inet.html#setopts-2""" } } - dtls_listener { - desc { - en: """""" - zh: """""" - } - } - dtls_listener_acceptors { desc { en: """Size of the acceptor pool.""" @@ -592,7 +564,7 @@ When set to false clients will be allowed to connect without authen } } - gateway_common_listener_mountpoint { + gateway_mountpoint { desc { en: """When publishing or subscribing, prefix all topics with a mountpoint string. The prefixed string will be removed from the topic name when the message is delivered to the subscriber. diff --git a/lib-ee/emqx_license/i18n/emqx_license_http_api.conf b/rel/i18n/emqx_license_http_api.hocon similarity index 100% rename from lib-ee/emqx_license/i18n/emqx_license_http_api.conf rename to rel/i18n/emqx_license_http_api.hocon diff --git a/lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf b/rel/i18n/emqx_license_schema.hocon similarity index 100% rename from lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf rename to rel/i18n/emqx_license_schema.hocon diff --git a/apps/emqx/i18n/emqx_limiter_i18n.conf b/rel/i18n/emqx_limiter_schema.hocon similarity index 100% rename from apps/emqx/i18n/emqx_limiter_i18n.conf rename to rel/i18n/emqx_limiter_schema.hocon diff --git a/apps/emqx_gateway/i18n/emqx_lwm2m_api_i18n.conf b/rel/i18n/emqx_lwm2m_api.hocon similarity index 100% rename from apps/emqx_gateway/i18n/emqx_lwm2m_api_i18n.conf rename to rel/i18n/emqx_lwm2m_api.hocon diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_alarms_i18n.conf b/rel/i18n/emqx_mgmt_api_alarms.hocon similarity index 100% rename from apps/emqx_management/i18n/emqx_mgmt_api_alarms_i18n.conf rename to rel/i18n/emqx_mgmt_api_alarms.hocon diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_banned_i18n.conf b/rel/i18n/emqx_mgmt_api_banned.hocon similarity index 94% rename from apps/emqx_management/i18n/emqx_mgmt_api_banned_i18n.conf rename to rel/i18n/emqx_mgmt_api_banned.hocon index 3045cb293..b45a40ba6 100644 --- a/apps/emqx_management/i18n/emqx_mgmt_api_banned_i18n.conf +++ b/rel/i18n/emqx_mgmt_api_banned.hocon @@ -87,8 +87,8 @@ emqx_mgmt_api_banned { } until { desc { - en: """The end time of the ban, the format is rfc3339, the default is the time when the operation was initiated + 5 minutes.""" - zh: """封禁的结束时间,式为 rfc3339,默认为发起操作的时间 + 5 分钟。""" + en: """The end time of the ban, the format is rfc3339, the default is the time when the operation was initiated + 1 year.""" + zh: """封禁的结束时间,格式为 rfc3339,默认值为发起操作的时间 + 1 年。""" } label { en: """Ban End Time""" diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf b/rel/i18n/emqx_mgmt_api_key_schema.hocon similarity index 100% rename from apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf rename to rel/i18n/emqx_mgmt_api_key_schema.hocon diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf b/rel/i18n/emqx_mgmt_api_publish.hocon similarity index 95% rename from apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf rename to rel/i18n/emqx_mgmt_api_publish.hocon index 4123ceefd..a09732cfc 100644 --- a/apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf +++ b/rel/i18n/emqx_mgmt_api_publish.hocon @@ -1,9 +1,7 @@ - emqx_mgmt_api_publish { publish_api { desc { - en: """Publish one message.
-Possible HTTP status response codes are:
+ en: """Possible HTTP status response codes are:
200: The message is delivered to at least one subscriber;
202: No matched subscribers;
400: Message is invalid. for example bad topic name, or QoS is out of range;
@@ -16,11 +14,14 @@ Possible HTTP status response codes are:
400: 消息编码错误,如非法主题,或 QoS 超出范围等。
503: 服务重启等过程中导致转发失败。""" } + label { + en: "Publish a message" + zh: "发布一条信息" + } } publish_bulk_api { desc { - en: """Publish a batch of messages.
-Possible HTTP response status code are:
+ en: """Possible HTTP response status code are:
200: All messages are delivered to at least one subscriber;
202: At least one message was not delivered to any subscriber;
400: At least one message is invalid. For example bad topic name, or QoS is out of range;
@@ -41,6 +42,10 @@ result of each individual message in the batch.""" /publish 是一样的。 如果所有的消息都是合法的,那么 HTTP 返回的内容是一个 JSON 数组,每个元素代表了该消息转发的状态。""" } + label { + en: "Publish a batch of messages" + zh: "发布一批信息" + } } topic_name { diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_status_i18n.conf b/rel/i18n/emqx_mgmt_api_status.hocon similarity index 94% rename from apps/emqx_management/i18n/emqx_mgmt_api_status_i18n.conf rename to rel/i18n/emqx_mgmt_api_status.hocon index fae17b35d..d72fd0998 100644 --- a/apps/emqx_management/i18n/emqx_mgmt_api_status_i18n.conf +++ b/rel/i18n/emqx_mgmt_api_status.hocon @@ -22,6 +22,10 @@ emqx_mgmt_api_status { "GET `/status`端点(没有`/api/...`前缀)也是这个端点的一个别名,工作方式相同。" " 这个别名从v5.0.0开始就有了。" } + label { + en: "Service health check" + zh: "服务健康检查" + } } get_status_response200 { diff --git a/apps/emqx_modules/i18n/emqx_modules_schema_i18n.conf b/rel/i18n/emqx_modules_schema.hocon similarity index 100% rename from apps/emqx_modules/i18n/emqx_modules_schema_i18n.conf rename to rel/i18n/emqx_modules_schema.hocon diff --git a/apps/emqx_plugins/i18n/emqx_plugins_schema.conf b/rel/i18n/emqx_plugins_schema.hocon similarity index 100% rename from apps/emqx_plugins/i18n/emqx_plugins_schema.conf rename to rel/i18n/emqx_plugins_schema.hocon diff --git a/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf b/rel/i18n/emqx_prometheus_schema.hocon similarity index 100% rename from apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf rename to rel/i18n/emqx_prometheus_schema.hocon diff --git a/apps/emqx_psk/i18n/emqx_psk_i18n.conf b/rel/i18n/emqx_psk_schema.hocon similarity index 100% rename from apps/emqx_psk/i18n/emqx_psk_i18n.conf rename to rel/i18n/emqx_psk_schema.hocon diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/rel/i18n/emqx_resource_schema.hocon similarity index 85% rename from apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf rename to rel/i18n/emqx_resource_schema.hocon index 600289b1d..933aa009b 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/rel/i18n/emqx_resource_schema.hocon @@ -45,6 +45,17 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise } } + resume_interval { + desc { + en: """The interval at which the buffer worker attempts to resend failed requests in the inflight window.""" + zh: """在发送失败后尝试重传飞行窗口中的请求的时间间隔。""" + } + label { + en: """Resume Interval""" + zh: """重试时间间隔""" + } + } + start_after_created { desc { en: """Whether start the resource right after created.""" @@ -135,14 +146,14 @@ When disabled the messages are buffered in RAM only.""" } } - async_inflight_window { + inflight_window { desc { - en: """Async query inflight window.""" - zh: """异步请求飞行队列窗口大小。""" + en: """Query inflight window. When query_mode is set to async, this config has to be set to 1 if messages from the same MQTT client have to be strictly ordered.""" + zh: """请求飞行队列窗口大小。当请求模式为异步时,如果需要严格保证来自同一 MQTT 客户端的消息有序,则必须将此值设为 1。""""" } label { - en: """Async inflight window""" - zh: """异步请求飞行队列窗口""" + en: """Inflight window""" + zh: """请求飞行队列窗口""" } } diff --git a/apps/emqx_retainer/i18n/emqx_retainer_api_i18n.conf b/rel/i18n/emqx_retainer_api.hocon similarity index 100% rename from apps/emqx_retainer/i18n/emqx_retainer_api_i18n.conf rename to rel/i18n/emqx_retainer_api.hocon diff --git a/apps/emqx_retainer/i18n/emqx_retainer_i18n.conf b/rel/i18n/emqx_retainer_schema.hocon similarity index 100% rename from apps/emqx_retainer/i18n/emqx_retainer_i18n.conf rename to rel/i18n/emqx_retainer_schema.hocon diff --git a/apps/emqx_modules/i18n/emqx_rewrite_api_i18n.conf b/rel/i18n/emqx_rewrite_api.hocon similarity index 100% rename from apps/emqx_modules/i18n/emqx_rewrite_api_i18n.conf rename to rel/i18n/emqx_rewrite_api.hocon diff --git a/apps/emqx_rule_engine/i18n/emqx_rule_api_schema.conf b/rel/i18n/emqx_rule_api_schema.hocon similarity index 99% rename from apps/emqx_rule_engine/i18n/emqx_rule_api_schema.conf rename to rel/i18n/emqx_rule_api_schema.hocon index e4c2314de..f9b344666 100644 --- a/apps/emqx_rule_engine/i18n/emqx_rule_api_schema.conf +++ b/rel/i18n/emqx_rule_api_schema.hocon @@ -35,8 +35,8 @@ emqx_rule_api_schema { event_username { desc { - en: "The User Name" - zh: "" + en: "Username" + zh: "用户名" } label: { en: "Username" diff --git a/apps/emqx_rule_engine/i18n/emqx_rule_engine_api.conf b/rel/i18n/emqx_rule_engine_api.hocon similarity index 100% rename from apps/emqx_rule_engine/i18n/emqx_rule_engine_api.conf rename to rel/i18n/emqx_rule_engine_api.hocon diff --git a/apps/emqx_rule_engine/i18n/emqx_rule_engine_schema.conf b/rel/i18n/emqx_rule_engine_schema.hocon similarity index 100% rename from apps/emqx_rule_engine/i18n/emqx_rule_engine_schema.conf rename to rel/i18n/emqx_rule_engine_schema.hocon diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/rel/i18n/emqx_schema.hocon similarity index 93% rename from apps/emqx/i18n/emqx_schema_i18n.conf rename to rel/i18n/emqx_schema.hocon index 5a48c218a..d36809c3b 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/rel/i18n/emqx_schema.hocon @@ -666,15 +666,15 @@ mqtt 下所有的配置作为全局的默认值存在,它可以被 zone< mqtt_idle_timeout { desc { - en: """After the TCP connection is established, if the MQTT CONNECT packet from the client is -not received within the time specified by idle_timeout, the connection will be disconnected. -After the CONNECT packet has been accepted by EMQX, if the connection idles for this long time, -then the Erlang process is put to hibernation to save OS resources. Note: long idle_timeout -interval may impose risk at the system if large number of malicious clients only establish connections -but do not send any data.""" - zh: """TCP 连接建立后,如果在 idle_timeout 指定的时间内未收到客户端的 MQTT CONNECT 报文,则连接将被断开。 -如果连接在 CONNECT 报文被 EMQX 接受之后空闲超过该时长,那么服务这个连接的 Erlang 进程会进入休眠以节省系统资源。 -注意,该配置值如果设置过大的情况下,如果大量恶意客户端只连接,但不发任何数据,可能会导致系统资源被恶意消耗。""" + en: """Configure the duration of time that a connection can remain idle (i.e., without any data transfer) before being: + - Automatically disconnected if no CONNECT package is received from the client yet. + - Put into hibernation mode to save resources if some CONNECT packages are already received. +Note: Please set the parameter with caution as long idle time will lead to resource waste.""" + zh: """设置连接被断开或进入休眠状态前的等待时间,空闲超时后, + - 如暂未收到客户端的 CONNECT 报文,连接将断开; + - 如已收到客户端的 CONNECT 报文,连接将进入休眠模式以节省系统资源。 + +注意:请合理设置该参数值,如等待时间设置过长,可能造成系统资源的浪费。""" } label: { en: """Idle Timeout""" @@ -783,8 +783,8 @@ but do not send any data.""" mqtt_ignore_loop_deliver { desc { - en: """Ignore loop delivery of messages for MQTT v3.1.1/v3.1.0, similar to No Local subscription option in MQTT 5.0.""" - zh: """是否为 MQTT v3.1.1/v3.1.0 客户端忽略投递自己发布的消息,类似于 MQTT 5.0 中的 No Local 订阅选项。""" + en: """Whether the messages sent by the MQTT v3.1.1/v3.1.0 client will be looped back to the publisher itself, similar to No Local in MQTT 5.0.""" + zh: """设置由 MQTT v3.1.1/v3.1.0 客户端发布的消息是否将转发给其本身;类似 MQTT 5.0 协议中的 No Local 选项。""" } label: { en: """Ignore Loop Deliver""" @@ -794,10 +794,10 @@ but do not send any data.""" mqtt_strict_mode { desc { - en: """Parse MQTT messages in strict mode. -When set to true, invalid utf8 strings in for example client ID, topic name, etc. will cause the client to be disconnected""" + en: """Whether to parse MQTT messages in strict mode. +In strict mode, invalid utf8 strings in for example client ID, topic name, etc. will cause the client to be disconnected.""" zh: """是否以严格模式解析 MQTT 消息。 -当设置为 true 时,例如客户端 ID、主题名称等中的无效 utf8 字符串将导致客户端断开连接。""" +严格模式下,如客户端 ID、主题名称等中包含无效 utf8 字符串,连接将被断开。""" } label: { en: """Strict Mode""" @@ -807,8 +807,10 @@ When set to true, invalid utf8 strings in for example client ID, topic name, etc mqtt_response_information { desc { - en: """Specify the response information returned to the client. This feature is disabled if is set to \"\". Applies only to clients using MQTT 5.0.""" - zh: """指定返回给客户端的响应信息。如果设置为 \"\",则禁用此功能。仅适用于使用 MQTT 5.0 协议的客户端。""" + en: """UTF-8 string, for creating the response topic, for example, if set to reqrsp/, the publisher/subscriber will communicate using the topic prefix reqrsp/. +To disable this feature, input \"\" in the text box below. Only applicable to MQTT 5.0 clients.""" + zh: """UTF-8 字符串,用于指定返回给客户端的响应主题,如 reqrsp/,此时请求和应答客户端都需要使用 reqrsp/ 前缀的主题来完成通讯。 +如希望禁用此功能,请在下方的文字框中输入\"\";仅适用于 MQTT 5.0 客户端。""" } label: { en: """Response Information""" @@ -818,23 +820,23 @@ When set to true, invalid utf8 strings in for example client ID, topic name, etc mqtt_server_keepalive { desc { - en: """The keep alive that EMQX requires the client to use. If configured as disabled, it means that the keep alive specified by the client will be used. Requires Server Keep Alive in MQTT 5.0, so it is only applicable to clients using MQTT 5.0 protocol.""" - zh: """EMQX 要求客户端使用的保活时间,配置为 disabled 表示将使用客户端指定的保活时间。需要用到 MQTT 5.0 中的 Server Keep Alive,因此仅适用于使用 MQTT 5.0 协议的客户端。""" + en: """The keep alive duration required by EMQX. To use the setting from the client side, choose disabled from the drop-down list. Only applicable to MQTT 5.0 clients.""" + zh: """EMQX 要求的保活时间,如设为 disabled,则将使用客户端指定的保持连接时间;仅适用于 MQTT 5.0 客户端。""" } label: { en: """Server Keep Alive""" - zh: """服务端保持连接""" + zh: """服务端保活时间""" } } mqtt_keepalive_backoff { desc { - en: """The backoff multiplier used by the broker to determine the client keep alive timeout. If EMQX doesn't receive any packet in Keep Alive * Backoff * 2 seconds, EMQX will close the current connection.""" - zh: """Broker 判定客户端保活超时使用的退避乘数。如果 EMQX 在 Keep Alive * Backoff * 2 秒内未收到任何报文,EMQX 将关闭当前连接。""" + en: """The coefficient EMQX uses to confirm whether the keep alive duration of the client expires. Formula: Keep Alive * Backoff * 2""" + zh: """EMQX 判定客户端保活超时使用的阈值系数。计算公式为:Keep Alive * Backoff * 2""" } label: { en: """Keep Alive Backoff""" - zh: """保持连接退避乘数""" + zh: """保活超时阈值系数""" } } @@ -978,14 +980,14 @@ To configure \"topic/1\" > \"topic/2\": mqtt_use_username_as_clientid { desc { - en: """Whether to user Client ID as Username. -This setting takes effect later than Use Peer Certificate as Username (peer_cert_as_username) and Use peer certificate as Client ID (peer_cert_as_clientid).""" + en: """Whether to use Username as Client ID. +This setting takes effect later than Use Peer Certificate as Username and Use peer certificate as Client ID.""" zh: """是否使用用户名作为客户端 ID。 -此设置的作用时间晚于 使用对端证书作为用户名peer_cert_as_username) 和 使用对端证书作为客户端 IDpeer_cert_as_clientid)。""" +此设置的作用时间晚于 对端证书作为用户名对端证书作为客户端 ID。""" } label: { en: """Use Username as Client ID""" - zh: """使用用户名作为客户端 ID""" + zh: """用户名作为客户端 ID""" } } @@ -993,22 +995,22 @@ This setting takes effect later than Use Peer Certificate as Usernamecn: Take the CN field of the certificate as Username -- dn: Take the DN field of the certificate as Username -- crt: Take the content of the DER or PEM certificate as Username -- pem: Convert DER certificate content to PEM format as Username -- md5: Take the MD5 value of the content of the DER or PEM certificate as Username""" - zh: """使用对端证书中的 CN、DN 字段或整个证书内容来作为用户名。仅适用于 TLS 连接。 -目前支持配置为以下内容: -- cn: 取证书的 CN 字段作为 Username -- dn: 取证书的 DN 字段作为 Username -- crt: 取 DERPEM 证书的内容作为 Username -- pem: 将 DER 证书内容转换为 PEM 格式后作为 Username -- md5: 取 DERPEM 证书的内容的 MD5 值作为 Username""" +- cn: CN field of the certificate +- dn: DN field of the certificate +- crt: Content of the DER or PEM certificate +- pem: Convert DER certificate content to PEM format and use as Username +- md5: MD5 value of the DER or PEM certificate""" + zh: """使用对端证书中的 CN、DN 字段或整个证书内容来作为用户名;仅适用于 TLS 连接。 +目前支持: +- cn: 取证书的 CN 字段 +- dn: 取证书的 DN 字段 +- crt: 取 DERPEM 的证书内容 +- pem: 将 DER 证书转换为 PEM 格式作为用户名 +- md5: 取 DERPEM 证书内容的 MD5 值""" } label: { en: """Use Peer Certificate as Username""" - zh: """使用对端证书作为用户名""" + zh: """对端证书作为用户名""" } } @@ -1016,22 +1018,22 @@ Supported configurations are the following: desc { en: """Use the CN, DN field in the peer certificate or the entire certificate content as Client ID. Only works for the TLS connection. Supported configurations are the following: -- cn: Take the CN field of the certificate as Client ID -- dn: Take the DN field of the certificate as Client ID -- crt: Take the content of the DER or PEM certificate as Client ID -- pem: Convert DER certificate content to PEM format as Client ID -- md5: Take the MD5 value of the content of the DER or PEM certificate as Client ID""" - zh: """使用对端证书中的 CN、DN 字段或整个证书内容来作为客户端 ID。仅适用于 TLS 连接。 -目前支持配置为以下内容: -- cn: 取证书的 CN 字段作为 Client ID -- dn: 取证书的 DN 字段作为 Client ID -- crt: 取 DERPEM 证书的内容作为 Client ID -- pem: 将 DER 证书内容转换为 PEM 格式后作为 Client ID -- md5: 取 DERPEM 证书的内容的 MD5 值作为 Client ID""" +- cn: CN field of the certificate +- dn: DN field of the certificate +- crt: DER or PEM certificate +- pem: Convert DER certificate content to PEM format and use as Client ID +- md5: MD5 value of the DER or PEM certificate""" + zh: """使用对端证书中的 CN、DN 字段或整个证书内容来作为客户端 ID。仅适用于 TLS 连接; +目前支持: +- cn: 取证书的 CN 字段 +- dn: 取证书的 DN 字段 +- crt: 取 DERPEM 证书的内容 +- pem: 将 DER 证书内容转换为 PEM 格式作为客户端 ID +- md5: 取 DERPEM 证书内容的 MD5 值""" } label: { en: """Use Peer Certificate as Client ID""" - zh: """使用对端证书作为客户端 ID""" + zh: """对端证书作为客户端 ID""" } } @@ -1082,8 +1084,8 @@ Supported configurations are the following: - `round_robin_per_group`:在共享组内循环选择下一个成员; - `local`:选择随机的本地成员,否则选择随机的集群范围内成员; - `sticky`:总是使用上次选中的订阅者派发,直到它断开连接; - - `hash_clientid`:使用发送者的 Client ID 进行 Hash 来选择订阅者; - - `hash_topic`:使用源主题进行 Hash 来选择订阅者。""" + - `hash_clientid`:通过对发送者的客户端 ID 进行 Hash 处理来选择订阅者; + - `hash_topic`:通过对源主题进行 Hash 处理来选择订阅者。""" } } @@ -1503,8 +1505,8 @@ In case PSK cipher suites are intended, make sure to configure common_ssl_opts_schema_hibernate_after { desc { - en: """ Hibernate the SSL process after idling for amount of time reducing its memory footprint. """ - zh: """ 在闲置一定时间后休眠 SSL 进程,减少其内存占用。""" + en: """Hibernate the SSL process after idling for amount of time reducing its memory footprint.""" + zh: """在闲置一定时间后休眠 SSL 进程,减少其内存占用。""" } label: { en: "hibernate after" @@ -1810,6 +1812,56 @@ server_ssl_opts_schema_ocsp_refresh_http_timeout { } } +server_ssl_opts_schema_enable_crl_check { + desc { + en: "Whether to enable CRL verification for this listener." + zh: "是否为该监听器启用 CRL 检查。" + } + label: { + en: "Enable CRL Check" + zh: "启用 CRL 检查" + } +} + +crl_cache_refresh_http_timeout { + desc { + en: "The timeout for the HTTP request when fetching CRLs. This is" + " a global setting for all listeners." + zh: "获取 CRLs 时 HTTP 请求的超时。 该配置对所有启用 CRL 检查的监听器监听器有效。" + } + label: { + en: "CRL Cache Refresh HTTP Timeout" + zh: "CRL 缓存刷新 HTTP 超时" + } +} + +crl_cache_refresh_interval { + desc { + en: "The period to refresh the CRLs from the servers. This is a global setting" + " for all URLs and listeners." + zh: "从服务器刷新CRL的周期。 该配置对所有 URL 和监听器有效。" + } + label: { + en: "CRL Cache Refresh Interval" + zh: "CRL 缓存刷新间隔" + } +} + +crl_cache_capacity { + desc { + en: "The maximum number of CRL URLs that can be held in cache. If the cache is at" + " full capacity and a new URL must be fetched, then it'll evict the oldest" + " inserted URL in the cache." + zh: "缓存中可容纳的 CRL URL 的最大数量。" + " 如果缓存的容量已满,并且必须获取一个新的 URL," + "那么它将驱逐缓存中插入的最老的 URL。" + } + label: { + en: "CRL Cache Capacity" + zh: "CRL 缓存容量" + } +} + fields_listeners_tcp { desc { en: """TCP listeners.""" diff --git a/apps/emqx_slow_subs/i18n/emqx_slow_subs_api_i18n.conf b/rel/i18n/emqx_slow_subs_api.hocon similarity index 100% rename from apps/emqx_slow_subs/i18n/emqx_slow_subs_api_i18n.conf rename to rel/i18n/emqx_slow_subs_api.hocon diff --git a/apps/emqx_slow_subs/i18n/emqx_slow_subs_i18n.conf b/rel/i18n/emqx_slow_subs_schema.hocon similarity index 100% rename from apps/emqx_slow_subs/i18n/emqx_slow_subs_i18n.conf rename to rel/i18n/emqx_slow_subs_schema.hocon diff --git a/apps/emqx_statsd/i18n/emqx_statsd_api_i18n.conf b/rel/i18n/emqx_statsd_api.hocon similarity index 100% rename from apps/emqx_statsd/i18n/emqx_statsd_api_i18n.conf rename to rel/i18n/emqx_statsd_api.hocon diff --git a/apps/emqx_statsd/i18n/emqx_statsd_schema_i18n.conf b/rel/i18n/emqx_statsd_schema.hocon similarity index 100% rename from apps/emqx_statsd/i18n/emqx_statsd_schema_i18n.conf rename to rel/i18n/emqx_statsd_schema.hocon diff --git a/apps/emqx_modules/i18n/emqx_telemetry_api_i18n.conf b/rel/i18n/emqx_telemetry_api.hocon similarity index 100% rename from apps/emqx_modules/i18n/emqx_telemetry_api_i18n.conf rename to rel/i18n/emqx_telemetry_api.hocon diff --git a/apps/emqx_modules/i18n/emqx_topic_metrics_api_i18n.conf b/rel/i18n/emqx_topic_metrics_api.hocon similarity index 95% rename from apps/emqx_modules/i18n/emqx_topic_metrics_api_i18n.conf rename to rel/i18n/emqx_topic_metrics_api.hocon index 623884f31..22f038d4e 100644 --- a/apps/emqx_modules/i18n/emqx_topic_metrics_api_i18n.conf +++ b/rel/i18n/emqx_topic_metrics_api.hocon @@ -1,7 +1,7 @@ emqx_topic_metrics_api { get_topic_metrics_api { desc { - en: """List Topic metrics""" + en: """List topic metrics""" zh: """获取主题监控数据""" } } @@ -15,21 +15,21 @@ emqx_topic_metrics_api { post_topic_metrics_api { desc { - en: """Create Topic metrics""" + en: """Create topic metrics""" zh: """创建主题监控数据""" } } gat_topic_metrics_data_api { desc { - en: """Get Topic metrics""" + en: """Get topic metrics""" zh: """获取主题监控数据""" } } delete_topic_metrics_data_api { desc { - en: """Delete Topic metrics""" + en: """Delete topic metrics""" zh: """删除主题监控数据""" } } @@ -43,7 +43,7 @@ emqx_topic_metrics_api { topic_metrics_api_response400 { desc { - en: """Bad Request. Already exists or bad topic name""" + en: """Bad request. Already exists or bad topic name""" zh: """错误请求。已存在或错误的主题名称""" } } diff --git a/scripts/apps-version-check.sh b/scripts/apps-version-check.sh index b86101027..2918ff5f7 100755 --- a/scripts/apps-version-check.sh +++ b/scripts/apps-version-check.sh @@ -36,7 +36,7 @@ for app in ${APPS}; do echo "IGNORE: $src_file is newly added" true elif [ "$old_app_version" = "$now_app_version" ]; then - changed_lines="$(git diff "$latest_release"...HEAD --ignore-blank-lines -G "$no_comment_re" \ + changed_lines="$(git diff "$latest_release" --ignore-blank-lines -G "$no_comment_re" \ -- "$app_path/src" \ -- "$app_path/include" \ -- ":(exclude)"$app_path/src/*.appup.src"" \ diff --git a/scripts/check-i18n-style.escript b/scripts/check-i18n-style.escript index 6ad6c1770..cbe79c82e 100755 --- a/scripts/check-i18n-style.escript +++ b/scripts/check-i18n-style.escript @@ -1,12 +1,15 @@ #!/usr/bin/env escript +%% called from check-i18n-style.sh + -mode(compile). --define(YELLOW, "\e[33m"). +% -define(YELLOW, "\e[33m"). % not used -define(RED, "\e[31m"). -define(RESET, "\e[39m"). main([Files0]) -> + io:format(user, "checking i18n file styles", []), _ = put(errors, 0), Files = string:tokens(Files0, "\n"), ok = load_hocon(), @@ -46,7 +49,7 @@ logerr(Fmt, Args) -> check(File) -> - io:format(user, "checking: ~s~n", [File]), + io:format(user, ".", []), {ok, C} = hocon:load(File), maps:foreach(fun check_one_field/2, C), ok. @@ -84,7 +87,7 @@ do_check_desc(Name, _) -> die("~s: missing 'zh' or 'en'~n", [Name]). check_desc_string(Name, Tr, <<>>) -> - io:format(standard_error, ?YELLOW ++ "WARNING: ~s.~s: empty string~n" ++ ?RESET, [Name, Tr]); + logerr("~s.~s: empty string~n", [Name, Tr]); check_desc_string(Name, Tr, BinStr) -> Str = unicode:characters_to_list(BinStr, utf8), Err = fun(Reason) -> diff --git a/scripts/check-i18n-style.sh b/scripts/check-i18n-style.sh index 0be565f30..d21f43a72 100755 --- a/scripts/check-i18n-style.sh +++ b/scripts/check-i18n-style.sh @@ -3,6 +3,6 @@ set -euo pipefail cd -P -- "$(dirname -- "$0")/.." -all_files="$(git ls-files '*i18n*.conf')" +all_files="$(git ls-files 'rel/i18n/*.hocon')" ./scripts/check-i18n-style.escript "$all_files" diff --git a/scripts/check-nl-at-eof.sh b/scripts/check-nl-at-eof.sh index 88f8f9c2e..8ca110c81 100755 --- a/scripts/check-nl-at-eof.sh +++ b/scripts/check-nl-at-eof.sh @@ -16,6 +16,9 @@ nl_at_eof() { scripts/erlfmt) return ;; + *.jks) + return + ;; esac local lastbyte lastbyte="$(tail -c 1 "$file" 2>&1)" diff --git a/scripts/ct/run.sh b/scripts/ct/run.sh index bf7b2073d..82823720d 100755 --- a/scripts/ct/run.sh +++ b/scripts/ct/run.sh @@ -170,6 +170,12 @@ for dep in ${CT_DEPS}; do dynamo) FILES+=( '.ci/docker-compose-file/docker-compose-dynamo.yaml' ) ;; + rocketmq) + FILES+=( '.ci/docker-compose-file/docker-compose-rocketmq.yaml' ) + ;; + cassandra) + FILES+=( '.ci/docker-compose-file/docker-compose-cassandra.yaml' ) + ;; *) echo "unknown_ct_dependency $dep" exit 1 diff --git a/scripts/get-dashboard.sh b/scripts/get-dashboard.sh index c3559865f..ace795aa5 100755 --- a/scripts/get-dashboard.sh +++ b/scripts/get-dashboard.sh @@ -20,7 +20,7 @@ case "$VERSION" in esac DASHBOARD_PATH='apps/emqx_dashboard/priv' -DASHBOARD_REPO='emqx-dashboard-web-new' +DASHBOARD_REPO='emqx-dashboard5' DIRECT_DOWNLOAD_URL="https://github.com/emqx/${DASHBOARD_REPO}/releases/download/${VERSION}/${RELEASE_ASSET_FILE}" case $(uname) in diff --git a/scripts/merge-i18n.escript b/scripts/merge-i18n.escript index 816cbe182..b2501d10a 100755 --- a/scripts/merge-i18n.escript +++ b/scripts/merge-i18n.escript @@ -4,12 +4,8 @@ main(_) -> BaseConf = <<"">>, - Cfgs0 = get_all_cfgs("apps/"), - Cfgs1 = get_all_cfgs("lib-ee/"), - Conf0 = merge(BaseConf, Cfgs0), - Conf = [merge(Conf0, Cfgs1), - io_lib:nl() - ], + Cfgs0 = get_all_files(), + Conf = merge(BaseConf, Cfgs0), OutputFile = "apps/emqx_dashboard/priv/i18n.conf", ok = filelib:ensure_dir(OutputFile), ok = file:write_file(OutputFile, Conf). @@ -25,39 +21,7 @@ merge(BaseConf, Cfgs) -> end end, BaseConf, Cfgs). -get_all_cfgs(Root) -> - Apps = filelib:wildcard("*", Root) -- ["emqx_machine"], - Dirs = [filename:join([Root, App]) || App <- Apps], - lists:foldl(fun get_cfgs/2, [], Dirs). - -get_all_cfgs(Dir, Cfgs) -> - Fun = fun(E, Acc) -> - Path = filename:join([Dir, E]), - get_cfgs(Path, Acc) - end, - lists:foldl(Fun, Cfgs, filelib:wildcard("*", Dir)). - -get_cfgs(Dir, Cfgs) -> - case filelib:is_dir(Dir) of - false -> - Cfgs; - _ -> - Files = filelib:wildcard("*", Dir), - case lists:member("i18n", Files) of - false -> - try_enter_child(Dir, Files, Cfgs); - true -> - EtcDir = filename:join([Dir, "i18n"]), - Confs = filelib:wildcard("*.conf", EtcDir), - NewCfgs = [filename:join([EtcDir, Name]) || Name <- Confs], - try_enter_child(Dir, Files, NewCfgs ++ Cfgs) - end - end. - -try_enter_child(Dir, Files, Cfgs) -> - case lists:member("src", Files) of - false -> - Cfgs; - true -> - get_all_cfgs(filename:join([Dir, "src"]), Cfgs) - end. +get_all_files() -> + Dir = filename:join(["rel","i18n"]), + Files = filelib:wildcard("*.hocon", Dir), + lists:map(fun(Name) -> filename:join([Dir, Name]) end, Files). diff --git a/scripts/rel/delete-old-changelog.sh b/scripts/rel/delete-old-changelog.sh new file mode 100755 index 000000000..4b0f4db2f --- /dev/null +++ b/scripts/rel/delete-old-changelog.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -euo pipefail + +[ "${DEBUG:-0}" = 1 ] && set -x + +top_dir="$(git rev-parse --show-toplevel)" +prev_ce_tag="$("$top_dir"/scripts/find-prev-rel-tag.sh 'emqx')" +prev_ee_tag="$("$top_dir"/scripts/find-prev-rel-tag.sh 'emqx-enterprise')" + +## check if a file is included in the previous release +is_released() { + file="$1" + prev_tag="$2" + # check if file exists in the previous release + if git show "$prev_tag:$file" >/dev/null 2>&1; then + return 1 + else + return 0 + fi +} + +## loop over files in $top_dir/changes/ce +## and delete the ones that are included in the previous ce and ee releases +while read -r file; do + if is_released "$file" "$prev_ce_tag" && is_released "$file" "$prev_ee_tag"; then + echo "deleting $file, released in $prev_ce_tag and $prev_ee_tag" + rm -f "$file" + fi +done < <(find "$top_dir/changes/ce" -type f -name '*.md') + +## loop over files in $top_dir/changes/ee +## and delete the ones taht are included in the previous ee release +while read -r file; do + if is_released "$file" "$prev_ee_tag"; then + echo "deleting $file, released in $prev_ee_tag" + rm -f "$file" + fi +done < <(find "$top_dir/changes/ee" -type f -name '*.md') diff --git a/scripts/relup-test/run-relup-lux.sh b/scripts/relup-test/run-relup-lux.sh index 570e58340..674eadc45 100755 --- a/scripts/relup-test/run-relup-lux.sh +++ b/scripts/relup-test/run-relup-lux.sh @@ -45,8 +45,8 @@ fi # From now on, no need for the v|e prefix OLD_VSN="${old_vsn#[e|v]}" -OLD_PKG="$(pwd)/_upgrade_base/${profile}-${OLD_VSN}-otp24.3.4.2-2-ubuntu20.04-amd64.tar.gz" -CUR_PKG="$(pwd)/_packages/${profile}/${profile}-${cur_vsn}-otp24.3.4.2-2-ubuntu20.04-amd64.tar.gz" +OLD_PKG="$(pwd)/_upgrade_base/${profile}-${OLD_VSN}-otp24.3.4.2-3-ubuntu20.04-amd64.tar.gz" +CUR_PKG="$(pwd)/_packages/${profile}/${profile}-${cur_vsn}-otp24.3.4.2-3-ubuntu20.04-amd64.tar.gz" if [ ! -f "$OLD_PKG" ]; then echo "$OLD_PKG not found" diff --git a/scripts/relup-test/start-relup-test-cluster.sh b/scripts/relup-test/start-relup-test-cluster.sh index 385137dc7..d22c61680 100755 --- a/scripts/relup-test/start-relup-test-cluster.sh +++ b/scripts/relup-test/start-relup-test-cluster.sh @@ -22,7 +22,7 @@ WEBHOOK="webhook.$NET" BENCH="bench.$NET" COOKIE='this-is-a-secret' ## Erlang image is needed to run webhook server and emqtt-bench -ERLANG_IMAGE="ghcr.io/emqx/emqx-builder/5.0-28:1.13.4-24.3.4.2-2-ubuntu20.04" +ERLANG_IMAGE="ghcr.io/emqx/emqx-builder/5.0-33:1.13.4-24.3.4.2-3-ubuntu20.04" # builder has emqtt-bench installed BENCH_IMAGE="$ERLANG_IMAGE" diff --git a/scripts/rerun-failed-checks.py b/scripts/rerun-failed-checks.py new file mode 100644 index 000000000..ff9b9f33e --- /dev/null +++ b/scripts/rerun-failed-checks.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# Usage: python3 rerun-failed-checks.py -t -r -b +# +# Description: This script will fetch the latest commit from a branch, and check the status of all check runs of the commit. +# If any check run is not successful, it will trigger a rerun of the failed jobs. +# +# Default branch is master, default repo is emqx/emqx +# +# Limitation: only works for upstream repo, not for forked. +import requests +import http.client +import json +import os +import sys +import time +import math +from optparse import OptionParser + +job_black_list = [ + 'windows', + 'publish_artifacts', + 'stale' +] + +def fetch_latest_commit(token: str, repo: str, branch: str): + url = f'https://api.github.com/repos/{repo}/commits/{branch}' + headers = {'Accept': 'application/vnd.github+json', + 'Authorization': f'Bearer {token}', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'python3' + } + r = requests.get(url, headers=headers) + if r.status_code == 200: + res = r.json() + return res + else: + print( + f'Failed to fetch latest commit from {branch} branch, code: {r.status_code}') + sys.exit(1) + + +''' +fetch check runs of a commit. +@note, only works for public repos +''' +def fetch_check_runs(token: str, repo: str, ref: str): + all_checks = [] + page = 1 + total_pages = 1 + per_page = 100 + failed_checks = [] + while page <= total_pages: + print(f'Fetching check runs for page {page} of {total_pages} pages') + url = f'https://api.github.com/repos/{repo}/commits/{ref}/check-runs?per_page={per_page}&page={page}' + headers = {'Accept': 'application/vnd.github.v3+json', + 'Authorization': f'Bearer {token}' + } + r = requests.get(url, headers=headers) + if r.status_code == 200: + resp = r.json() + all_checks.extend(resp['check_runs']) + + page += 1 + if 'total_count' in resp and resp['total_count'] > per_page: + total_pages = math.ceil(resp['total_count'] / per_page) + else: + print(f'Failed to fetch check runs {r.status_code}') + sys.exit(1) + + + for crun in all_checks: + if crun['status'] == 'completed' and crun['conclusion'] != 'success': + print('Failed check: ', crun['name']) + failed_checks.append( + {'id': crun['id'], 'name': crun['name'], 'url': crun['url']}) + else: + # pretty print crun + # print(json.dumps(crun, indent=4)) + print('successed:', crun['id'], crun['name'], + crun['status'], crun['conclusion']) + + return failed_checks + +''' +rerquest a check-run +''' +def trigger_build(failed_checks: list, repo: str, token: str): + reruns = [] + for crun in failed_checks: + if crun['name'].strip() in job_black_list: + print(f'Skip black listed job {crun["name"]}') + continue + + r = requests.get(crun['url'], headers={'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'python3', + 'Authorization': f'Bearer {token}'} + ) + if r.status_code == 200: + # url example: https://github.com/qzhuyan/emqx/actions/runs/4469557961/jobs/7852858687 + run_id = r.json()['details_url'].split('/')[-3] + reruns.append(run_id) + else: + print(f'failed to fetch check run {crun["name"]}') + + # remove duplicates + for run_id in set(reruns): + url = f'https://api.github.com/repos/{repo}/actions/runs/{run_id}/rerun-failed-jobs' + + r = requests.post(url, headers={'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'python3', + 'Authorization': f'Bearer {token}'} + ) + if r.status_code == 201: + print(f'Successfully triggered build for {crun["name"]}') + + else: + # Only complain but not exit. + print( + f'Failed to trigger rerun for {run_id}, {crun["name"]}: {r.status_code} : {r.text}') + + +def main(): + parser = OptionParser() + parser.add_option("-r", "--repo", dest="repo", + help="github repo", default="emqx/emqx") + parser.add_option("-t", "--token", dest="gh_token", + help="github API token") + parser.add_option("-b", "--branch", dest="branch", default='master', + help="Branch that workflow runs on") + (options, args) = parser.parse_args() + + # Get gh token from env var GITHUB_TOKEN if provided, else use the one from command line + token = os.environ['GITHUB_TOKEN'] if 'GITHUB_TOKEN' in os.environ else options.gh_token + + target_commit = fetch_latest_commit(token, options.repo, options.branch) + + failed_checks = fetch_check_runs(token, options.repo, target_commit['sha']) + + trigger_build(failed_checks, options.repo, token) + + +if __name__ == '__main__': + main() diff --git a/scripts/spellcheck/dicts/emqx.txt b/scripts/spellcheck/dicts/emqx.txt index b027f92ec..79c8b7e3a 100644 --- a/scripts/spellcheck/dicts/emqx.txt +++ b/scripts/spellcheck/dicts/emqx.txt @@ -271,3 +271,5 @@ nif TDengine clickhouse FormatType +RocketMQ +Keyspace diff --git a/scripts/test/emqx-boot.bats b/scripts/test/emqx-boot.bats new file mode 100644 index 000000000..96f9a4457 --- /dev/null +++ b/scripts/test/emqx-boot.bats @@ -0,0 +1,21 @@ +#!/usr/bin/env bats + +# https://github.com/bats-core/bats-core +# env PROFILE=emqx bats -t -p --verbose-run scripts/test/emqx-boot.bats + +@test "PROFILE must be set" { + [[ -n "$PROFILE" ]] +} + +@test "emqx boot with invalid node name" { + output="$(env EMQX_NODE_NAME="invliadename#" ./_build/$PROFILE/rel/emqx/bin/emqx console 2>&1|| true)" + [[ "$output" =~ "ERROR: Invalid node name,".+ ]] +} + +@test "corrupted cluster config file" { + conffile="./_build/$PROFILE/rel/emqx/data/configs/cluster-override.conf" + echo "{" > $conffile + run ./_build/$PROFILE/rel/emqx/bin/emqx console + [[ $status -ne 0 ]] + rm -f $conffile +} diff --git a/scripts/test/emqx-smoke-test.sh b/scripts/test/emqx-smoke-test.sh index 361137bc0..ce8116b39 100755 --- a/scripts/test/emqx-smoke-test.sh +++ b/scripts/test/emqx-smoke-test.sh @@ -8,6 +8,7 @@ IP=$1 PORT=$2 URL="http://$IP:$PORT/status" +## Check if EMQX is responding ATTEMPTS=10 while ! curl "$URL" >/dev/null 2>&1; do if [ $ATTEMPTS -eq 0 ]; then @@ -17,3 +18,26 @@ while ! curl "$URL" >/dev/null 2>&1; do sleep 5 ATTEMPTS=$((ATTEMPTS-1)) done + +## Check if the API docs are available +API_DOCS_URL="http://$IP:$PORT/api-docs/index.html" +API_DOCS_STATUS="$(curl -s -o /dev/null -w "%{http_code}" "$API_DOCS_URL")" +if [ "$API_DOCS_STATUS" != "200" ]; then + echo "emqx is not responding on $API_DOCS_URL" + exit 1 +fi + +## Check if the swagger.json contains hidden fields +## fail if it does +SWAGGER_JSON_URL="http://$IP:$PORT/api-docs/swagger.json" +## assert swagger.json is valid json +JSON="$(curl -s "$SWAGGER_JSON_URL")" +echo "$JSON" | jq . >/dev/null + +if [ "${EMQX_SMOKE_TEST_CHECK_HIDDEN_FIELDS:-yes}" = 'yes' ]; then + ## assert swagger.json does not contain trie_compaction (which is a hidden field) + if echo "$JSON" | grep -q trie_compaction; then + echo "swagger.json contains hidden fields" + exit 1 + fi +fi diff --git a/scripts/test/influx/influx-bridge.conf b/scripts/test/influx/influx-bridge.conf index df10a0ec6..0416e42b6 100644 --- a/scripts/test/influx/influx-bridge.conf +++ b/scripts/test/influx/influx-bridge.conf @@ -6,7 +6,7 @@ bridges { org = "emqx" precision = "ms" resource_opts { - async_inflight_window = 100 + inflight_window = 100 auto_restart_interval = "60s" batch_size = 100 batch_time = "10ms" diff --git a/scripts/test/start-two-nodes-in-docker.sh b/scripts/test/start-two-nodes-in-docker.sh index c174bc630..53689b207 100755 --- a/scripts/test/start-two-nodes-in-docker.sh +++ b/scripts/test/start-two-nodes-in-docker.sh @@ -10,19 +10,36 @@ set -euo pipefail # ensure dir cd -P -- "$(dirname -- "$0")/../../" -IMAGE1="${1}" -IMAGE2="${2:-${IMAGE1}}" +HAPROXY_PORTS=(-p 18083:18083 -p 8883:8883 -p 8084:8084) NET='emqx.io' NODE1="node1.$NET" NODE2="node2.$NET" COOKIE='this-is-a-secret' -## clean up -docker rm -f haproxy >/dev/null 2>&1 || true -docker rm -f "$NODE1" >/dev/null 2>&1 || true -docker rm -f "$NODE2" >/dev/null 2>&1 || true -docker network rm "$NET" >/dev/null 2>&1 || true +cleanup() { + docker rm -f haproxy >/dev/null 2>&1 || true + docker rm -f "$NODE1" >/dev/null 2>&1 || true + docker rm -f "$NODE2" >/dev/null 2>&1 || true + docker network rm "$NET" >/dev/null 2>&1 || true +} + +while getopts ":Pc" opt +do + case $opt in + # -P option is treated similarly to docker run -P: + # publish ports to random available host ports + P) HAPROXY_PORTS=(-p 18083 -p 8883 -p 8084);; + c) cleanup; exit 0;; + *) ;; + esac +done +shift $((OPTIND - 1)) + +IMAGE1="${1}" +IMAGE2="${2:-${IMAGE1}}" + +cleanup docker network create "$NET" @@ -128,18 +145,18 @@ backend emqx_wss_back EOF -docker run -d --name haproxy \ - --net "$NET" \ - -v "$(pwd)/tmp/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg" \ - -v "$(pwd)/apps/emqx/etc/certs:/usr/local/etc/haproxy/certs" \ - -w /usr/local/etc/haproxy \ - -p 18083:18083 \ - -p 8883:8883 \ - -p 8084:8084 \ - "haproxy:2.4" \ - bash -c 'set -euo pipefail; - cat certs/cert.pem certs/key.pem > /tmp/emqx.pem; - haproxy -f haproxy.cfg' +haproxy_cid=$(docker run -d --name haproxy \ + --net "$NET" \ + -v "$(pwd)/tmp/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg" \ + -v "$(pwd)/apps/emqx/etc/certs:/usr/local/etc/haproxy/certs" \ + -w /usr/local/etc/haproxy \ + "${HAPROXY_PORTS[@]}" \ + "haproxy:2.4" \ + bash -c 'set -euo pipefail; + cat certs/cert.pem certs/key.pem > /tmp/emqx.pem; + haproxy -f haproxy.cfg') + +haproxy_ssl_port=$(docker inspect --format='{{(index (index .NetworkSettings.Ports "8084/tcp") 0).HostPort}}' "$haproxy_cid") wait_limit=60 wait_for_emqx() { @@ -165,7 +182,7 @@ wait_for_haproxy() { -CAfile apps/emqx/etc/certs/cacert.pem \ -cert apps/emqx/etc/certs/cert.pem \ -key apps/emqx/etc/certs/key.pem \ - localhost:8084