Skip to content

Commit 9ce257c

Browse files
authored
xpk storage detach (#420)
* drop manifest requirement when attaching gcsfuse storage * docs * rename `storage delete` to `storage detach * do not require manifest in fuse workflow tests * fix: redundant --cluster parameter * change 'user' to 'you' * unify gcsfuse template generation algo to match filestore * gcsfuse storage attach add missing size in docs * dromp manifest requirement for attaching filestore storage * remove obsolete `--manifest` param * fix: linting and typing * fix: linting * separate filestore instance name from storage volume names * fix: typo * minor fixes * remove obsolete error messages * docs * fix: readme inconsistencies * fix: add project id to storage manifest file path * refactor create_storage_crds * use Availability.ZONAL instead of hardcoded string value * move loading filestore location logic to load_location function * fix: fuse test messages * fix tests: always remove fuse storage after failing tests * filestore delete * fix error caused by same storage names * checking if filestore has attached storages before deletion * fix: bugs in filestore name passing * xpk storage delete docs * remove filestore deletion at the end of tests, because it is now handled by xpk storage delete * missing kind arguments for storage delete * fix: func name typo from merging * fix: typo in func name from merging (again) * add loading instance when prompted for fullname * add verification of filestore deletion to tests * automatic detach when deleting filestore, user prompt * change filestore delete msg
1 parent 1ee61f5 commit 9ce257c

File tree

5 files changed

+140
-17
lines changed

5 files changed

+140
-17
lines changed

.github/workflows/reusable_storage_tests_by_type.yaml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,16 @@ jobs:
185185
run: python3 xpk.py workload list --cluster ${{inputs.cluster-name}} --zone=${{inputs.zone}} --wait-for-job-completion $FS_DELETE_WORKLOAD --timeout 300
186186
- name: Delete the delete workload on the cluster
187187
run: python3 xpk.py workload delete --workload $FS_DELETE_WORKLOAD --cluster ${{inputs.cluster-name}} --zone=${{inputs.zone}}
188-
- name: Delete storage
188+
- name: Detach storage volumes
189189
if: always()
190-
run: python3 xpk.py storage delete ${{secrets.STORAGE_NAME}} --zone=${{inputs.zone}} --cluster=${{inputs.cluster-name}}
190+
run: python3 xpk.py storage detach ${{secrets.STORAGE_NAME}} --cluster=${{inputs.cluster-name}} --zone=${{inputs.zone}}
191191
- name: Verify VolumeBundle deleted
192192
run: |
193193
! kubectl get volumebundle | grep ${{secrets.STORAGE_NAME}}
194-
- name: Delete the filestore instance
195-
if: always() && inputs.storage-command == 'create'
196-
run: gcloud filestore instances delete ${{secrets.STORAGE_NAME}} --zone=${{inputs.zone}} --force
194+
- name: Delete GCP Filestore Storage instance
195+
if: always() && inputs.storage-command == 'create' && inputs.storage-type == 'gcpfilestore'
196+
run: python3 xpk.py storage delete ${{secrets.STORAGE_NAME}} --cluster=${{inputs.cluster-name}} --zone=${{inputs.zone}}
197+
- name: Verify deletion of GCP Filestore Storage instance
198+
if: inputs.storage-command == 'create' && inputs.storage-type == 'gcpfilestore'
199+
run: |
200+
! gcloud filestore instances list | grep ${{secrets.STORAGE_NAME}}

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,10 +520,20 @@ python3 xpk.py workload create \
520520
--project=$PROJECT --cluster=$CLUSTER --zone=$ZONE \
521521
--tpu-type=v5litepod-16 --storage=test-storage
522522
```
523+
524+
### Detaching storage
525+
526+
```shell
527+
python3 xpk.py storage detach $STORAGE_NAME \
528+
--project=$PROJECT --cluster=$CLUSTER --zone=$ZONE
529+
```
530+
523531
### Deleting storage
524532
533+
XPK allows you to remove Filestore instances easily with `xpk storage delete` command. **Warning:** this deletes all data contained in the Filestore!
534+
525535
```shell
526-
python3 xpk.py storage delete $STORAGE_NAME \
536+
python3 xpk.py storage delete test-fs-instance \
527537
--project=$PROJECT --cluster=$CLUSTER --zone=$ZONE
528538
```
529539

src/xpk/commands/storage.py

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from argparse import Namespace
1818

1919
from kubernetes import client as k8s_client
20+
from kubernetes.client import ApiClient
2021
from kubernetes.client.rest import ApiException
2122

2223
from ..core import gcsfuse
@@ -42,26 +43,30 @@
4243
STORAGE_CRD_PLURAL,
4344
XPK_API_GROUP_NAME,
4445
XPK_API_GROUP_VERSION,
46+
Storage,
4547
create_storage_crds,
4648
get_storage,
4749
list_storages,
4850
print_storages_for_cluster,
4951
)
50-
from ..utils.console import xpk_exit, xpk_print
52+
from ..utils.console import get_user_input, xpk_exit, xpk_print
5153
from ..utils.kubectl import apply_kubectl_manifest
5254

5355

5456
def storage_create(args: Namespace) -> None:
5557
add_zone_and_project(args)
5658
if args.type == GCP_FILESTORE_TYPE:
57-
filestore_client = FilestoreClient(args.zone, args.name, args.project)
59+
if args.instance is None:
60+
args.instance = args.name
61+
62+
filestore_client = FilestoreClient(args.zone, args.instance, args.project)
5863
filestore_exists = filestore_client.check_instance_exists()
5964
if filestore_exists:
60-
xpk_print(f"Filestore instance {args.name} already exists.")
65+
xpk_print(f"Filestore instance {args.instance} already exists.")
6166
xpk_exit(1)
6267
filestore_network = get_cluster_network(args)
6368
xpk_print(
64-
f"Creating Filestore instance {args.name} in network:"
69+
f"Creating Filestore instance {args.instance} in network:"
6570
f" {filestore_network}"
6671
)
6772
filestore_client.create_instance(
@@ -85,6 +90,40 @@ def storage_create(args: Namespace) -> None:
8590
apply_kubectl_manifest(k8s_api_client, manifest)
8691

8792

93+
def storage_delete(args: Namespace) -> None:
94+
add_zone_and_project(args)
95+
k8s_api_client = setup_k8s_env(args)
96+
storages = list_storages(k8s_api_client)
97+
filestore_client = FilestoreClient(args.zone, args.name, args.project)
98+
99+
if not filestore_client.check_instance_exists():
100+
xpk_print(f"Filestore instance {args.name} does not exist.")
101+
xpk_exit(1)
102+
103+
filestore_instance_name = filestore_client.get_instance_fullname()
104+
105+
children = [
106+
storage
107+
for storage in storages
108+
if storage.bucket.startswith(filestore_instance_name)
109+
]
110+
111+
if children and not args.force:
112+
detach = get_user_input(
113+
"Deleting a filestore storage will destroy your filestore instance and"
114+
" all its data in all volumes will be lost. Do you wish to delete the"
115+
f" filestore instance {filestore_instance_name}?\n y (yes) / n (no):\n'"
116+
)
117+
if not detach:
118+
xpk_print("Deleting storage canceled.")
119+
xpk_exit(0)
120+
121+
for child in children:
122+
delete_storage_resources(k8s_api_client, child)
123+
124+
filestore_client.delete_filestore_instance()
125+
126+
88127
def storage_attach(args: Namespace) -> None:
89128
add_zone_and_project(args)
90129
if args.type == GCP_FILESTORE_TYPE:
@@ -142,6 +181,12 @@ def storage_list(args: Namespace) -> None:
142181
print_storages_for_cluster(storages)
143182

144183

184+
def storage_detach(args: Namespace) -> None:
185+
k8s_api_client = setup_k8s_env(args)
186+
storage = get_storage(k8s_api_client, args.name)
187+
delete_storage_resources(k8s_api_client, storage)
188+
189+
145190
def delete_resource(api_call, resource_name: str, resource_kind: str) -> None:
146191
"""
147192
Deletes a Kubernetes resource and handles potential API exceptions.
@@ -167,27 +212,34 @@ def delete_resource(api_call, resource_name: str, resource_kind: str) -> None:
167212
xpk_print(f"Deleted {resource_kind}:{resource_name}")
168213

169214

170-
def storage_delete(args: Namespace) -> None:
171-
k8s_api_client = setup_k8s_env(args)
215+
def delete_storage_resources(k8s_api_client: ApiClient, storage: Storage):
216+
"""
217+
Deletes storage PV, PVC, SC and custom resources (if they exist).
218+
219+
Args:
220+
k8s_api_client: An ApiClient object for interacting with the Kubernetes API.
221+
storage: Storage to delete
222+
"""
172223
api_instance = k8s_client.CustomObjectsApi(k8s_api_client)
173224
core_api = k8s_client.CoreV1Api()
174225
storage_api = k8s_client.StorageV1Api()
175-
storage = get_storage(k8s_api_client, args.name)
226+
176227
delete_resource(
177228
lambda name: core_api.delete_namespaced_persistent_volume_claim(
178229
name, "default"
179230
),
180231
storage.pvc,
181232
"Persistent Volume Claim",
182233
)
234+
183235
delete_resource(
184236
core_api.delete_persistent_volume, storage.pv, "Persistent Volume"
185237
)
186238

187239
if storage.type == GCP_FILESTORE_TYPE:
188240
delete_resource(
189241
storage_api.delete_storage_class,
190-
get_storage_class_name(args.name),
242+
get_storage_class_name(storage.name),
191243
"Storage Class",
192244
)
193245

@@ -199,7 +251,7 @@ def storage_delete(args: Namespace) -> None:
199251
version=KJOB_API_GROUP_VERSION,
200252
plural=KJOB_API_VOLUME_BUNDLE_PLURAL,
201253
),
202-
args.name,
254+
storage.name,
203255
"VolumeBundle",
204256
)
205257

@@ -210,6 +262,6 @@ def storage_delete(args: Namespace) -> None:
210262
version=XPK_API_GROUP_VERSION,
211263
plural=STORAGE_CRD_PLURAL,
212264
),
213-
args.name,
265+
storage.name,
214266
"Storage",
215267
)

src/xpk/core/filestore.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def load_instance(self) -> None:
110110

111111
def get_instance_location(self) -> str:
112112
"""Get Filestore instance's location"""
113+
self.load_instance()
113114
return str(self.instance.name.split("/")[3])
114115

115116
def create_instance(
@@ -174,6 +175,21 @@ def create_instance(
174175
f"Filestore instance {self.get_instance_fullname(location)} created"
175176
)
176177

178+
def delete_filestore_instance(self):
179+
# Initialize request
180+
name = self.get_instance_fullname()
181+
request = filestore_v1.DeleteInstanceRequest(name=name)
182+
183+
# Make the request
184+
operation = self._client.delete_instance(request)
185+
xpk_print("Waiting for filestore deletion to complete...")
186+
try:
187+
operation.result()
188+
except GoogleCloudError as e:
189+
xpk_print(f"Error while deleting Filestore instance: {e}")
190+
xpk_exit(1)
191+
xpk_print(f"Filestore instance {name} deleted")
192+
177193
def create_sc(self, name: str, network: str) -> dict:
178194
"""Create a yaml representing filestore StorageClass."""
179195
data = templates.load(FS_SC_PATH)

src/xpk/parser/storage.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
storage_attach,
2121
storage_create,
2222
storage_delete,
23+
storage_detach,
2324
storage_list,
2425
)
2526
from .common import (
@@ -40,8 +41,9 @@ def set_storage_parser(storage_parser: argparse.ArgumentParser) -> None:
4041
)
4142
add_storage_attach_parser(storage_subcommands)
4243
add_storage_list_parser(storage_subcommands)
43-
add_storage_delete_parser(storage_subcommands)
44+
add_storage_detach_parser(storage_subcommands)
4445
add_storage_create_parser(storage_subcommands)
46+
add_storage_delete_parser(storage_subcommands)
4547

4648

4749
def add_storage_attach_parser(
@@ -199,6 +201,14 @@ def add_storage_create_parser(
199201
'Optional Arguments',
200202
'Optional arguments for storage create.',
201203
)
204+
opt_args.add_argument(
205+
'--instance',
206+
type=str,
207+
help=(
208+
'(optional) Name of the filestore instance. If not set, then the'
209+
' "name" parameter is infered as an instance name.'
210+
),
211+
)
202212
opt_args.add_argument(
203213
'--vol',
204214
type=str,
@@ -250,6 +260,31 @@ def add_storage_list_parser(
250260
)
251261

252262

263+
def add_storage_detach_parser(
264+
storage_subcommands_parser: argparse.ArgumentParser,
265+
):
266+
storage_detach_parser: argparse.ArgumentParser = (
267+
storage_subcommands_parser.add_parser(
268+
'detach', help='Detach XPK Storage.'
269+
)
270+
)
271+
storage_detach_parser.set_defaults(func=storage_detach)
272+
add_shared_arguments(storage_detach_parser)
273+
274+
req_args = storage_detach_parser.add_argument_group(
275+
'Required Arguments',
276+
'Arguments required for storage detach.',
277+
)
278+
req_args.add_argument('name', type=str)
279+
add_cluster_arguments(req_args, required=True)
280+
281+
opt_args = storage_detach_parser.add_argument_group(
282+
'Optional Arguments',
283+
'Optional arguments for storage delete.',
284+
)
285+
add_kind_cluster_arguments(opt_args)
286+
287+
253288
def add_storage_delete_parser(
254289
storage_subcommands_parser: argparse.ArgumentParser,
255290
):
@@ -272,4 +307,10 @@ def add_storage_delete_parser(
272307
'Optional Arguments',
273308
'Optional arguments for storage delete.',
274309
)
310+
opt_args.add_argument(
311+
'--force',
312+
'-f',
313+
action='store_true',
314+
help='Force filestore instance deletion even if it has attached storages',
315+
)
275316
add_kind_cluster_arguments(opt_args)

0 commit comments

Comments
 (0)