-
Notifications
You must be signed in to change notification settings - Fork 0
valkey test container #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Valkey | ||
|
|
||
| Since testcontainers-python <a href="https://github.com/testcontainers/testcontainers-python/releases/tag/v4.14.0"><span class="tc-version">:material-tag: v4.14.0</span></a> | ||
|
|
||
| ## Introduction | ||
|
|
||
| The Testcontainers module for Valkey. | ||
|
|
||
| ## Adding this module to your project dependencies | ||
|
|
||
| Please run the following command to add the Valkey module to your python dependencies: | ||
|
|
||
| ```bash | ||
| pip install testcontainers[valkey] | ||
| ``` | ||
|
|
||
| ## Usage example | ||
|
|
||
| <!--codeinclude--> | ||
|
|
||
| [Creating a Valkey container](../../modules/valkey/example_basic.py) | ||
|
|
||
| <!--/codeinclude--> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| .. autoclass:: testcontainers.valkey.ValkeyContainer | ||
| .. title:: testcontainers.valkey.ValkeyContainer |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import socket | ||
|
|
||
| from testcontainers.valkey import ValkeyContainer | ||
|
|
||
|
|
||
| def basic_example(): | ||
| with ValkeyContainer() as valkey_container: | ||
| # Get connection parameters | ||
| host = valkey_container.get_host() | ||
| port = valkey_container.get_exposed_port() | ||
| connection_url = valkey_container.get_connection_url() | ||
|
|
||
| print(f"Valkey connection URL: {connection_url}") | ||
| print(f"Host: {host}, Port: {port}") | ||
|
|
||
| # Connect using raw socket and RESP protocol | ||
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
| s.connect((host, port)) | ||
|
|
||
| # PING command | ||
| s.sendall(b"*1\r\n$4\r\nPING\r\n") | ||
| response = s.recv(1024) | ||
| print(f"PING response: {response.decode()}") | ||
|
|
||
| # SET command | ||
| s.sendall(b"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n") | ||
| response = s.recv(1024) | ||
| print(f"SET response: {response.decode()}") | ||
|
|
||
| # GET command | ||
| s.sendall(b"*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n") | ||
| response = s.recv(1024) | ||
| print(f"GET response: {response.decode()}") | ||
|
|
||
|
|
||
| def password_example(): | ||
| with ValkeyContainer().with_password("mypassword") as valkey_container: | ||
| host = valkey_container.get_host() | ||
| port = valkey_container.get_exposed_port() | ||
| connection_url = valkey_container.get_connection_url() | ||
|
|
||
| print(f"\nValkey with password connection URL: {connection_url}") | ||
|
|
||
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
| s.connect((host, port)) | ||
|
|
||
| # AUTH command | ||
| s.sendall(b"*2\r\n$4\r\nAUTH\r\n$10\r\nmypassword\r\n") | ||
| response = s.recv(1024) | ||
| print(f"AUTH response: {response.decode()}") | ||
|
|
||
| # PING after auth | ||
| s.sendall(b"*1\r\n$4\r\nPING\r\n") | ||
| response = s.recv(1024) | ||
| print(f"PING response: {response.decode()}") | ||
|
|
||
|
|
||
| def version_example(): | ||
| # Using specific version | ||
| with ValkeyContainer().with_image_tag("8.0") as valkey_container: | ||
| print(f"\nUsing image: {valkey_container.image}") | ||
| connection_url = valkey_container.get_connection_url() | ||
| print(f"Connection URL: {connection_url}") | ||
|
|
||
|
|
||
| def bundle_example(): | ||
| # Using bundle with all modules (JSON, Bloom, Search, etc.) | ||
| with ValkeyContainer().with_bundle() as valkey_container: | ||
| print(f"\nUsing bundle image: {valkey_container.image}") | ||
| host = valkey_container.get_host() | ||
| port = valkey_container.get_exposed_port() | ||
|
|
||
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
| s.connect((host, port)) | ||
| s.sendall(b"*1\r\n$4\r\nPING\r\n") | ||
| response = s.recv(1024) | ||
| print(f"PING response: {response.decode()}") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| basic_example() | ||
| password_example() | ||
| version_example() | ||
| bundle_example() | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,138 @@ | ||||||||||||||
| # | ||||||||||||||
| # 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. | ||||||||||||||
|
|
||||||||||||||
| import socket | ||||||||||||||
| from typing import Optional | ||||||||||||||
|
|
||||||||||||||
| from testcontainers.core.container import DockerContainer | ||||||||||||||
| from testcontainers.core.waiting_utils import wait_container_is_ready | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class ValkeyNotReady(Exception): | ||||||||||||||
| pass | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class ValkeyContainer(DockerContainer): | ||||||||||||||
| """ | ||||||||||||||
| Valkey container. | ||||||||||||||
|
|
||||||||||||||
| Example: | ||||||||||||||
|
|
||||||||||||||
| .. doctest:: | ||||||||||||||
|
|
||||||||||||||
| >>> from testcontainers.valkey import ValkeyContainer | ||||||||||||||
|
|
||||||||||||||
| >>> with ValkeyContainer() as valkey_container: | ||||||||||||||
| ... connection_url = valkey_container.get_connection_url() | ||||||||||||||
| """ | ||||||||||||||
|
|
||||||||||||||
| def __init__(self, image: str = "valkey/valkey:latest", port: int = 6379, **kwargs) -> None: | ||||||||||||||
| super().__init__(image, **kwargs) | ||||||||||||||
| self.port = port | ||||||||||||||
| self.password: Optional[str] = None | ||||||||||||||
| self.with_exposed_ports(self.port) | ||||||||||||||
|
|
||||||||||||||
| def with_password(self, password: str) -> "ValkeyContainer": | ||||||||||||||
| """ | ||||||||||||||
| Configure authentication for Valkey. | ||||||||||||||
|
|
||||||||||||||
| Args: | ||||||||||||||
| password: Password for Valkey authentication. | ||||||||||||||
|
|
||||||||||||||
| Returns: | ||||||||||||||
| self: Container instance for method chaining. | ||||||||||||||
| """ | ||||||||||||||
| self.password = password | ||||||||||||||
| self.with_command(f"valkey-server --requirepass {password}") | ||||||||||||||
|
||||||||||||||
| Method | Shell Parsing | Special Chars | Injection Risk |
|---|---|---|---|
| String (current) | ✓ Yes | Interpreted | ❌ HIGH |
| List (suggested) | ✗ No | Literal | ✅ SAFE |
References:
- Docker best practices: https://docs.docker.com/engine/reference/builder/#cmd
- OWASP Injection: https://owasp.org/Top10/A03_2021-Injection/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deprecated Decorator Usage
Location: modules/valkey/testcontainers/valkey/__init__.py, line 113
Current Code:
@wait_container_is_ready(ValkeyNotReady)
def _connect(self) -> None:
"""Wait for Valkey to be ready by sending PING command."""
# ... connection logicIssue:
The @wait_container_is_ready decorator is deprecated in the codebase. Recent commits show active migration away from this pattern:
- PR feat: Add ExecWaitStrategy and migrate Postgres from deprecated decorator testcontainers/testcontainers-python#935: Postgres migrated to
ExecWaitStrategy - PR fix(minio): Use wait strategy instead of deprecated decorator testcontainers/testcontainers-python#899: Minio migrated to wait strategies
- PR fix(elasticsearch): Use wait strategy instead of deprecated decorator testcontainers/testcontainers-python#915: Elasticsearch migrated to wait strategies
Evidence from Codebase:
# From pytest.ini_options filterwarnings:
"ignore:The @wait_container_is_ready decorator is deprecated.*:DeprecationWarning"Recommended Fix:
Migrate to modern wait strategy pattern:
from testcontainers.core.wait_strategies import ExecWaitStrategy
class ValkeyContainer(DockerContainer):
def __init__(self, image: str = "valkey/valkey:latest", port: int = 6379, **kwargs) -> None:
super().__init__(image, **kwargs)
self.port = port
self.password: Optional[str] = None
self.with_exposed_ports(self.port)
def start(self) -> "ValkeyContainer":
# Build wait strategy based on password
if self.password:
# Use custom wait strategy for authenticated connections
self.waiting_for(self._create_auth_wait_strategy())
else:
# Use exec strategy for simple PING
self.waiting_for(
ExecWaitStrategy(["valkey-cli", "ping"])
)
super().start()
return selfBenefits:
- Aligns with project direction
- More composable and testable
- Consistent with other modules
- Better error messages
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import socket | ||
|
|
||
| from testcontainers.valkey import ValkeyContainer | ||
|
|
||
|
|
||
| def test_docker_run_valkey(): | ||
| with ValkeyContainer() as valkey: | ||
| host = valkey.get_host() | ||
| port = valkey.get_exposed_port() | ||
|
|
||
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
| s.connect((host, port)) | ||
| s.sendall(b"*1\r\n$4\r\nPING\r\n") | ||
| response = s.recv(1024) | ||
| assert b"+PONG" in response | ||
|
|
||
|
|
||
| def test_docker_run_valkey_with_password(): | ||
| with ValkeyContainer().with_password("mypass") as valkey: | ||
| host = valkey.get_host() | ||
| port = valkey.get_exposed_port() | ||
|
|
||
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
| s.connect((host, port)) | ||
| # Authenticate | ||
| s.sendall(b"*2\r\n$4\r\nAUTH\r\n$6\r\nmypass\r\n") | ||
| auth_response = s.recv(1024) | ||
| assert b"+OK" in auth_response | ||
|
|
||
| # Test SET command | ||
| s.sendall(b"*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n") | ||
| set_response = s.recv(1024) | ||
| assert b"+OK" in set_response | ||
|
|
||
| # Test GET command | ||
| s.sendall(b"*2\r\n$3\r\nGET\r\n$5\r\nhello\r\n") | ||
| get_response = s.recv(1024) | ||
| assert b"world" in get_response | ||
|
|
||
|
|
||
| def test_get_connection_url(): | ||
| with ValkeyContainer() as valkey: | ||
| url = valkey.get_connection_url() | ||
| assert url.startswith("valkey://") | ||
| assert str(valkey.get_exposed_port()) in url | ||
|
|
||
|
|
||
| def test_get_connection_url_with_password(): | ||
| with ValkeyContainer().with_password("secret") as valkey: | ||
| url = valkey.get_connection_url() | ||
| assert url.startswith("valkey://:secret@") | ||
| assert str(valkey.get_exposed_port()) in url | ||
|
|
||
|
|
||
| def test_with_image_tag(): | ||
| container = ValkeyContainer().with_image_tag("8.0") | ||
| assert "valkey/valkey:8.0" in container.image | ||
|
|
||
|
|
||
| def test_with_bundle(): | ||
| container = ValkeyContainer().with_bundle() | ||
| assert container.image == "valkey/valkey-bundle:latest" | ||
|
|
||
|
|
||
| def test_with_bundle_and_tag(): | ||
| container = ValkeyContainer().with_bundle().with_image_tag("9.0") | ||
| assert container.image == "valkey/valkey-bundle:9.0" | ||
|
|
||
|
|
||
| def test_bundle_starts(): | ||
| with ValkeyContainer().with_bundle() as valkey: | ||
| host = valkey.get_host() | ||
| port = valkey.get_exposed_port() | ||
|
|
||
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
| s.connect((host, port)) | ||
| s.sendall(b"*1\r\n$4\r\nPING\r\n") | ||
| response = s.recv(1024) | ||
| assert b"+PONG" in response |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid Raw Socket Usage in Examples
Location:
modules/valkey/example_basic.pyCurrent Approach:
Issues:
Recommendation:
Consider using Valkey-Glide client library for cleaner examples. Kiro should be able to easily convert these examples to Valkey-Glide
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed