diff --git a/.circleci/config.yml b/.circleci/config.yml index 0f6ad41b..d792f16e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,6 +49,7 @@ commands: cp -r script/config/examples/tls/sentinel1 ./sentinel1_tls cp -r script/config/examples/tls/sentinel2 ./sentinel2_tls cp -r script/config/examples/tls/sentinel3 ./sentinel3_tls + mkdir -m 1777 ./run - run: name: Start services command: | @@ -119,9 +120,15 @@ workflows: - test_single: requires: - install_dependencies + - test_single_unix: + requires: + - install_dependencies - test_sentinels: requires: - install_dependencies + - test_acl_unix: + requires: + - install_dependencies - test_tls: requires: - install_dependencies @@ -161,6 +168,18 @@ jobs: - start_services: services: redis-master - run_tests + test_single_unix: + executor: + name: ubuntu_vm + working_directory: ~/project + environment: + CONFIG_QUEUES_MASTER_NAME: unix://run/redis.sock + CONFIG_REDIS_PROXY: unix://run/redis.sock + steps: + - *attach-to-workspace + - start_services: + services: redis-master + - run_tests test_sentinels: executor: name: ubuntu_vm @@ -175,6 +194,22 @@ jobs: - start_services: services: redis-master redis-replica1 redis-replica2 redis-sentinel1 redis-sentinel2 redis-sentinel3 - run_tests + test_acl_unix: + executor: + name: ubuntu_vm + working_directory: ~/project + environment: + CONFIG_QUEUES_MASTER_NAME: unix://run/redis.sock + CONFIG_QUEUES_USERNAME: porta + CONFIG_QUEUES_PASSWORD: sup3rS3cre1! + CONFIG_REDIS_PROXY: unix://run/redis.sock + CONFIG_REDIS_USERNAME: porta + CONFIG_REDIS_PASSWORD: sup3rS3cre1! + steps: + - *attach-to-workspace + - start_services: + services: tls-redis-master + - run_tests test_tls: executor: name: ubuntu_vm diff --git a/lib/3scale/backend/async_redis/client.rb b/lib/3scale/backend/async_redis/client.rb index 8a4ac205..cf6640e1 100644 --- a/lib/3scale/backend/async_redis/client.rb +++ b/lib/3scale/backend/async_redis/client.rb @@ -16,6 +16,19 @@ class << self # @param opts [Hash] Redis connection options # @return [Async::Redis::Client] def call(opts) + return connect_tcp(opts) if url_present?(opts[:url]) + + connect_unix(opts) + end + alias :connect :call + + private + + def url_present?(url) + !url.to_s.strip.empty? + end + + def connect_tcp(opts) uri = URI(opts[:url]) credentials = [ uri.user || opts[:username], uri.password || opts[:password]] @@ -28,11 +41,24 @@ def call(opts) else host = uri.host || EndpointHelpers::DEFAULT_HOST port = uri.port || EndpointHelpers::DEFAULT_PORT - endpoint = EndpointHelpers.prepare_endpoint(host, port, opts[:ssl], opts[:ssl_params]) + endpoint = EndpointHelpers.prepare_endpoint(host: host, port: port, ssl: opts[:ssl], ssl_params: opts[:ssl_params]) + Async::Redis::Client.new(endpoint, protocol: protocol, limit: opts[:max_connections]) + end + end + + def connect_unix(opts) + path = opts[:path] + + credentials = [opts[:username], opts[:password]] + protocol = Protocol::ExtendedRESP2.new(credentials: credentials) + + if opts.key? :sentinels + raise InvalidURI.new(path, 'unix paths are not supported for sentinels') + else + endpoint = EndpointHelpers.prepare_endpoint(path: path, ssl: opts[:ssl], ssl_params: opts[:ssl_params]) Async::Redis::Client.new(endpoint, protocol: protocol, limit: opts[:max_connections]) end end - alias :connect :call end end end diff --git a/lib/3scale/backend/async_redis/endpoint_helpers.rb b/lib/3scale/backend/async_redis/endpoint_helpers.rb index 0edcd30b..c17b4774 100644 --- a/lib/3scale/backend/async_redis/endpoint_helpers.rb +++ b/lib/3scale/backend/async_redis/endpoint_helpers.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'async/io' +require 'async/io/unix_endpoint' module ThreeScale module Backend @@ -14,20 +15,37 @@ class << self # @param host [String] # @param port [Integer] + # @param path [String] + # @param ssl [Boolean] # @param ssl_params [Hash] # @return [Async::IO::Endpoint::Generic] - def prepare_endpoint(host, port, ssl = false, ssl_params = nil) + def prepare_endpoint(**kwargs) + host_present?(kwargs[:host]) ? prepare_tcp_endpoint(**kwargs) : prepare_unix_endpoint(**kwargs) + end + + private + + def prepare_tcp_endpoint(host: nil, port: nil, ssl: false, ssl_params: nil) tcp_endpoint = Async::IO::Endpoint.tcp(host, port) - if ssl - ssl_context = OpenSSL::SSL::SSLContext.new - ssl_context.set_params(format_ssl_params(ssl_params)) if ssl_params - return Async::IO::SSLEndpoint.new(tcp_endpoint, ssl_context: ssl_context) - end + return prepare_ssl_endpoint(endpoint: tcp_endpoint, ssl_params: ssl_params) if ssl tcp_endpoint end + def prepare_unix_endpoint(path: '', ssl: false, ssl_params: nil) + unix_endpoint = Async::IO::Endpoint.unix(path, Socket::PF_UNIX) + return unix_endpoint unless ssl + + prepare_ssl_endpoint(endpoint: unix_endpoint, ssl_params: ssl_params) + end + + def prepare_ssl_endpoint(endpoint: nil, ssl_params: nil) + ssl_context = OpenSSL::SSL::SSLContext.new + ssl_context.set_params(format_ssl_params(ssl_params)) if ssl_params + Async::IO::SSLEndpoint.new(endpoint, ssl_context: ssl_context) + end + def format_ssl_params(ssl_params) cert = ssl_params[:cert].to_s.strip key = ssl_params[:key].to_s.strip @@ -39,6 +57,10 @@ def format_ssl_params(ssl_params) updated_ssl_params end + + def host_present?(host) + !host.to_s.strip.empty? + end end end end diff --git a/lib/3scale/backend/async_redis/sentinels_client_acl_tls.rb b/lib/3scale/backend/async_redis/sentinels_client_acl_tls.rb index 24d2ab7a..48a17e4f 100644 --- a/lib/3scale/backend/async_redis/sentinels_client_acl_tls.rb +++ b/lib/3scale/backend/async_redis/sentinels_client_acl_tls.rb @@ -10,7 +10,7 @@ class SentinelsClientACLTLS < Async::Redis::SentinelsClient def initialize(uri, protocol = Async::Redis::Protocol::RESP2, config, **options) @master_name = uri.host @sentinel_endpoints = config[:sentinels].map do |sentinel| - EndpointHelpers.prepare_endpoint(sentinel[:host], sentinel[:port], config[:ssl], config[:ssl_params]) + EndpointHelpers.prepare_endpoint(host: sentinel[:host], port: sentinel[:port], ssl: config[:ssl], ssl_params: config[:ssl_params]) end @role = config[:role] || :master @@ -32,7 +32,7 @@ def resolve_master next end - return EndpointHelpers.prepare_endpoint(address[0], address[1], @config[:ssl], @config[:ssl_params]) if address + return EndpointHelpers.prepare_endpoint(host: address[0], port: address[1], ssl:@config[:ssl], ssl_params: @config[:ssl_params]) if address end nil @@ -52,7 +52,7 @@ def resolve_slave next if slaves.empty? slave = select_slave(slaves) - return EndpointHelpers.prepare_endpoint(slave['ip'], slave['port'], @config[:ssl], @config[:ssl_params]) + return EndpointHelpers.prepare_endpoint(host: slave['ip'], port: slave['port'], ssl: @config[:ssl], ssl_params: @config[:ssl_params]) end nil diff --git a/script/config/examples/tls/master.conf b/script/config/examples/tls/master.conf index 0b14f10e..4661e9a8 100644 --- a/script/config/examples/tls/master.conf +++ b/script/config/examples/tls/master.conf @@ -1,4 +1,6 @@ port 0 +unixsocket /var/run/redis/redis.sock +unixsocketperm 777 tls-port 46380 tls-cert-file /etc/redis.crt tls-key-file /etc/redis.key diff --git a/script/config/podman-compose.yml b/script/config/podman-compose.yml index 96ccb0b2..3877494a 100644 --- a/script/config/podman-compose.yml +++ b/script/config/podman-compose.yml @@ -4,7 +4,9 @@ services: image: redis:6.2-alpine container_name: redis-master network_mode: host - command: [ redis-server, --port, "6379" ] + volumes: + - ./run:/var/run/redis:z + command: [ redis-server, --port, "6379", --unixsocket, "/var/run/redis/redis.sock", --unixsocketperm, "777" ] redis-replica1: image: redis:6.2-alpine @@ -47,6 +49,7 @@ services: container_name: tls-redis-master network_mode: host volumes: + - ./run:/var/run/redis:z - ./master.conf:/etc/redis.conf:z - ./circleci.crt:/etc/redis.crt:z - ./circleci.key:/etc/redis.key:z diff --git a/test/unit/storage_async_test.rb b/test/unit/storage_async_test.rb index d98e351e..0c28faf4 100644 --- a/test/unit/storage_async_test.rb +++ b/test/unit/storage_async_test.rb @@ -25,6 +25,12 @@ def test_redis_url assert_client_config(config_obj, storage) end + def test_redis_unix + config_obj = url('unix:///tmp/redis_unix.6379.sock') + storage = StorageAsync::Client.send :new, config_obj + assert_client_config(config_obj, storage) + end + def test_redis_protected_url assert_nothing_raised do StorageAsync::Client.send :new, url('redis://user:passwd@127.0.0.1:6379/0') @@ -303,13 +309,17 @@ def create_certs(alg) def assert_client_config(conf, conn, test_cert_type = nil) client = conn.instance_variable_get(:@inner).instance_variable_get(:@redis_async) - url = URI(conf[:url]) - host, port = client.endpoint.address - assert_equal url.host, host - assert_equal url.port, port - - db = client.protocol.instance_variable_get(:@db) - assert_equal conf[:db], db + if conf[:url].to_s.strip.empty? + path = conf[:path] + assert_equal path, client.endpoint.path + else + url = URI(conf[:url]) + host, port = client.endpoint.address + assert_equal url.host, host + assert_equal url.port, port + db = client.protocol.instance_variable_get(:@db) + assert_equal conf[:db], db + end assert_acl_credentials(conf, client) assert_tls_certs(conf, client, test_cert_type)