diff --git a/storagecontrol/conftest.py b/storagecontrol/conftest.py index d5958a74a2..5a350b3003 100644 --- a/storagecontrol/conftest.py +++ b/storagecontrol/conftest.py @@ -13,12 +13,17 @@ # limitations under the License. import os +import time import uuid +from google.api_core import exceptions +from google.api_core import retry from google.cloud import storage import pytest +import logging + @pytest.fixture(scope="module") def project_id() -> str: @@ -66,6 +71,46 @@ def hns_enabled_bucket(project_id: str, bucket_name: str) -> storage.Bucket: bucket.delete(force=True) +@pytest.fixture(scope="module") +def rapid_cache_zone() -> str: + yield os.environ.get("RAPID_CACHE_ZONE", "us-central1-a") + + +@pytest.fixture(scope="function") +def regional_hns_enabled_bucket( + project_id: str, bucket_name: str, rapid_cache_zone: str +) -> storage.Bucket: + """ + Yields and auto-cleans up a regional HNS enabled bucket located in the + region of the Rapid Cache zone. + """ + + storage_client = storage.Client(project=project_id) + bucket = storage_client.bucket(bucket_name) + bucket.iam_configuration.uniform_bucket_level_access_enabled = True + bucket.hierarchical_namespace_enabled = True + region = rapid_cache_zone.rsplit("-", 1)[0] + bucket = storage_client.create_bucket(bucket, location=region) + # Wait for the new bucket's metadata to propagate before creating a Rapid + # Cache on it. + time.sleep(20) + + yield bucket + + # A bucket cannot be deleted until its disabled Rapid Cache has been fully + # removed, which can take more than an hour. Cleanup is best-effort: a + # short retry, after which the bucket is left behind for later cleanup. + try: + retry.Retry( + predicate=retry.if_exception_type(exceptions.Conflict), + initial=10.0, + maximum=30.0, + timeout=120.0, + )(bucket.delete)(force=True) + except exceptions.GoogleAPIError as e: + logging.warning("Could not delete bucket %s: %s", bucket.name, e) + + @pytest.fixture(scope="function") def ubla_enabled_bucket(project_id: str, bucket_name: str) -> storage.Bucket: """ diff --git a/storagecontrol/create_rapid_cache.py b/storagecontrol/create_rapid_cache.py new file mode 100644 index 0000000000..81d80601b3 --- /dev/null +++ b/storagecontrol/create_rapid_cache.py @@ -0,0 +1,66 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://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 sys + +# [START storage_control_create_rapid_cache] +from google.cloud import storage_control_v2 +from google.protobuf import duration_pb2 + + +def create_rapid_cache(bucket_name: str, zone: str) -> None: + # The ID of your GCS bucket + # bucket_name = "your-unique-bucket-name" + + # The zone in which to create the Rapid Cache. The zone must be in the + # same region as the bucket. + # zone = "us-central1-a" + + storage_control_client = storage_control_v2.StorageControlClient() + # The storage bucket path uses the global access pattern, in which the "_" + # denotes this bucket exists in the global namespace. + project_path = storage_control_client.common_project_path("_") + bucket_path = f"{project_path}/buckets/{bucket_name}" + # The Rapid Cache ID must be the same as the zone. + rapid_cache_path = storage_control_client.rapid_cache_path( + project="_", bucket=bucket_name, rapid_cache=zone + ) + + # Rapid Cache Ultra requires a bucket with hierarchical namespace enabled. + rapid_cache = storage_control_v2.RapidCache( + name=rapid_cache_path, + zone=zone, + cache_type="rapid-cache-ultra", + # The time to live (TTL) for cache entries: 86400 seconds (24 hours). + ttl=duration_pb2.Duration(seconds=86400), + ) + request = storage_control_v2.CreateRapidCacheRequest( + parent=bucket_path, + rapid_cache=rapid_cache, + ) + + # Creating a Rapid Cache is a long-running operation. + operation = storage_control_client.create_rapid_cache(request=request) + response = operation.result() + + print(f"Created Rapid Cache: {response.name}") + print(f"State: {response.state}") + print(f"Cache type: {response.cache_type}") + + +# [END storage_control_create_rapid_cache] + + +if __name__ == "__main__": + create_rapid_cache(bucket_name=sys.argv[1], zone=sys.argv[2]) diff --git a/storagecontrol/disable_rapid_cache.py b/storagecontrol/disable_rapid_cache.py new file mode 100644 index 0000000000..f5b89d10f0 --- /dev/null +++ b/storagecontrol/disable_rapid_cache.py @@ -0,0 +1,56 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://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 sys + +# [START storage_control_disable_rapid_cache] +from google.cloud import storage_control_v2 + + +def disable_rapid_cache( + bucket_name: str, cache_id: str, timeout: float | None = None +) -> None: + # The ID of your GCS bucket + # bucket_name = "your-unique-bucket-name" + + # The ID of the Rapid Cache, which is the same as its zone + # cache_id = "us-central1-a" + + # (Optional) The maximum time to wait for the operation, in seconds. + # None waits until the operation completes. + # timeout = None + + storage_control_client = storage_control_v2.StorageControlClient() + # The storage bucket path uses the global access pattern, in which the "_" + # denotes this bucket exists in the global namespace. + rapid_cache_path = storage_control_client.rapid_cache_path( + project="_", bucket=bucket_name, rapid_cache=cache_id + ) + + request = storage_control_v2.DisableRapidCacheRequest(name=rapid_cache_path) + + # Disabling a Rapid Cache is a long-running operation. This sample waits + # for the operation to complete. + operation = storage_control_client.disable_rapid_cache(request=request) + response = operation.result(timeout=timeout) + + print(f"Disabled Rapid Cache: {response.name}") + print(f"State: {response.state}") + + +# [END storage_control_disable_rapid_cache] + + +if __name__ == "__main__": + disable_rapid_cache(bucket_name=sys.argv[1], cache_id=sys.argv[2]) diff --git a/storagecontrol/get_rapid_cache.py b/storagecontrol/get_rapid_cache.py new file mode 100644 index 0000000000..eeb8cfe2e1 --- /dev/null +++ b/storagecontrol/get_rapid_cache.py @@ -0,0 +1,48 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://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 sys + +# [START storage_control_get_rapid_cache] +from google.cloud import storage_control_v2 + + +def get_rapid_cache(bucket_name: str, cache_id: str) -> None: + # The ID of your GCS bucket + # bucket_name = "your-unique-bucket-name" + + # The ID of the Rapid Cache, which is the same as its zone + # cache_id = "us-central1-a" + + storage_control_client = storage_control_v2.StorageControlClient() + # The storage bucket path uses the global access pattern, in which the "_" + # denotes this bucket exists in the global namespace. + rapid_cache_path = storage_control_client.rapid_cache_path( + project="_", bucket=bucket_name, rapid_cache=cache_id + ) + + request = storage_control_v2.GetRapidCacheRequest(name=rapid_cache_path) + response = storage_control_client.get_rapid_cache(request=request) + + print(f"Got Rapid Cache: {response.name}") + print(f"Zone: {response.zone}") + print(f"State: {response.state}") + print(f"Cache type: {response.cache_type}") + + +# [END storage_control_get_rapid_cache] + + +if __name__ == "__main__": + get_rapid_cache(bucket_name=sys.argv[1], cache_id=sys.argv[2]) diff --git a/storagecontrol/list_rapid_caches.py b/storagecontrol/list_rapid_caches.py new file mode 100644 index 0000000000..4ed9eb9a15 --- /dev/null +++ b/storagecontrol/list_rapid_caches.py @@ -0,0 +1,45 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://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 sys + +# [START storage_control_list_rapid_caches] +from google.cloud import storage_control_v2 + + +def list_rapid_caches(bucket_name: str) -> None: + # The ID of your GCS bucket + # bucket_name = "your-unique-bucket-name" + + storage_control_client = storage_control_v2.StorageControlClient() + # The storage bucket path uses the global access pattern, in which the "_" + # denotes this bucket exists in the global namespace. + project_path = storage_control_client.common_project_path("_") + bucket_path = f"{project_path}/buckets/{bucket_name}" + + request = storage_control_v2.ListRapidCachesRequest(parent=bucket_path) + + # The returned pager handles pagination automatically. + page_result = storage_control_client.list_rapid_caches(request=request) + for rapid_cache in page_result: + print(rapid_cache.name) + + print(f"Listed Rapid Caches in bucket {bucket_name}") + + +# [END storage_control_list_rapid_caches] + + +if __name__ == "__main__": + list_rapid_caches(bucket_name=sys.argv[1]) diff --git a/storagecontrol/rapid_cache_test.py b/storagecontrol/rapid_cache_test.py new file mode 100644 index 0000000000..605930e147 --- /dev/null +++ b/storagecontrol/rapid_cache_test.py @@ -0,0 +1,110 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://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 concurrent.futures + +import create_rapid_cache +import disable_rapid_cache +import get_rapid_cache +from google.api_core import exceptions +from google.cloud import storage +from google.cloud import storage_control_v2 +import list_rapid_caches +import pytest +import update_rapid_cache + +import logging + +# The disable operation only completes once the cache has been fully removed, +# which can take more than an hour. The cache reaches the "disabled" state +# within seconds, so the test bounds the wait and then checks the state. +_DISABLE_TIMEOUT_SECONDS = 600 + + +def test_rapid_cache_create_get_list_update_disable( + capsys: pytest.CaptureFixture[str], + regional_hns_enabled_bucket: storage.Bucket, + rapid_cache_zone: str, +) -> None: + bucket_name = regional_hns_enabled_bucket.name + # The Rapid Cache ID is the same as its zone. + cache_id = rapid_cache_zone + cache_name = storage_control_v2.StorageControlClient.rapid_cache_path( + project="_", bucket=bucket_name, rapid_cache=cache_id + ) + disable_requested = False + + try: + # Test create Rapid Cache + create_rapid_cache.create_rapid_cache( + bucket_name=bucket_name, zone=rapid_cache_zone + ) + out, _ = capsys.readouterr() + assert f"Created Rapid Cache: {cache_name}" in out + assert "State: running" in out + assert "Cache type: rapid-cache-ultra" in out + + # Test get Rapid Cache + get_rapid_cache.get_rapid_cache(bucket_name=bucket_name, cache_id=cache_id) + out, _ = capsys.readouterr() + assert f"Got Rapid Cache: {cache_name}" in out + assert f"Zone: {rapid_cache_zone}" in out + assert "State: running" in out + assert "Cache type: rapid-cache-ultra" in out + + # Test list Rapid Caches + list_rapid_caches.list_rapid_caches(bucket_name=bucket_name) + out, _ = capsys.readouterr() + assert cache_name in out + + # Test update Rapid Cache + update_rapid_cache.update_rapid_cache( + bucket_name=bucket_name, cache_id=cache_id, ttl_seconds=172800 + ) + out, _ = capsys.readouterr() + assert f"Updated Rapid Cache: {cache_name}" in out + assert "TTL: 172800 seconds" in out + + # Test disable Rapid Cache + disable_requested = True + try: + disable_rapid_cache.disable_rapid_cache( + bucket_name=bucket_name, + cache_id=cache_id, + timeout=_DISABLE_TIMEOUT_SECONDS, + ) + out, _ = capsys.readouterr() + assert f"Disabled Rapid Cache: {cache_name}" in out + assert "State: disabled" in out + except concurrent.futures.TimeoutError: + logging.warning( + "Disable operation for %s did not complete within %s seconds; " + "checking the cache state instead.", + cache_name, + _DISABLE_TIMEOUT_SECONDS, + ) + capsys.readouterr() + get_rapid_cache.get_rapid_cache(bucket_name=bucket_name, cache_id=cache_id) + out, _ = capsys.readouterr() + assert "State: disabled" in out + finally: + if not disable_requested: + # Disable the Rapid Cache so that the bucket can be deleted once the + # cache has been fully removed. Errors are ignored. + try: + storage_control_v2.StorageControlClient().disable_rapid_cache( + name=cache_name + ) + except exceptions.GoogleAPIError as e: + logging.warning("Could not disable %s: %s", cache_name, e) diff --git a/storagecontrol/requirements.txt b/storagecontrol/requirements.txt index e7b93b6c24..e2bea4c66d 100644 --- a/storagecontrol/requirements.txt +++ b/storagecontrol/requirements.txt @@ -1 +1 @@ -google-cloud-storage-control==1.11.0 \ No newline at end of file +google-cloud-storage-control==1.17.0 \ No newline at end of file diff --git a/storagecontrol/update_rapid_cache.py b/storagecontrol/update_rapid_cache.py new file mode 100644 index 0000000000..2267a1e7bd --- /dev/null +++ b/storagecontrol/update_rapid_cache.py @@ -0,0 +1,69 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://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 sys + +# [START storage_control_update_rapid_cache] +from google.cloud import storage_control_v2 +from google.protobuf import duration_pb2 +from google.protobuf import field_mask_pb2 + + +def update_rapid_cache(bucket_name: str, cache_id: str, ttl_seconds: int) -> None: + # The ID of your GCS bucket + # bucket_name = "your-unique-bucket-name" + + # The ID of the Rapid Cache, which is the same as its zone + # cache_id = "us-central1-a" + + # The new time to live (TTL) for cache entries, in seconds. The TTL must + # be between 1 hour and 7 days. + # ttl_seconds = 172800 # 2 days + + storage_control_client = storage_control_v2.StorageControlClient() + # The storage bucket path uses the global access pattern, in which the "_" + # denotes this bucket exists in the global namespace. + rapid_cache_path = storage_control_client.rapid_cache_path( + project="_", bucket=bucket_name, rapid_cache=cache_id + ) + + rapid_cache = storage_control_v2.RapidCache( + name=rapid_cache_path, + ttl=duration_pb2.Duration(seconds=ttl_seconds), + ) + # Only the fields listed in the update mask are changed. + update_mask = field_mask_pb2.FieldMask(paths=["ttl"]) + request = storage_control_v2.UpdateRapidCacheRequest( + rapid_cache=rapid_cache, + update_mask=update_mask, + ) + + # Updating a Rapid Cache is a long-running operation. + operation = storage_control_client.update_rapid_cache(request=request) + response = operation.result() + + print(f"Updated Rapid Cache: {response.name}") + # The client library returns the TTL as a datetime.timedelta. + print(f"TTL: {response.ttl.total_seconds():.0f} seconds") + + +# [END storage_control_update_rapid_cache] + + +if __name__ == "__main__": + update_rapid_cache( + bucket_name=sys.argv[1], + cache_id=sys.argv[2], + ttl_seconds=int(sys.argv[3]), + )