From: Naman Munet Date: Tue, 4 Mar 2025 10:26:21 +0000 (+0530) Subject: mgr/dashboard: bucket lifecycle fixes after using xmltodict package X-Git-Tag: v20.3.0~297^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=8f7f923e945bb466461caa581ef0edef0d276e3c;p=ceph.git mgr/dashboard: bucket lifecycle fixes after using xmltodict package Fixes: https://tracker.ceph.com/issues/70275 Signed-off-by: Naman Munet --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-lifecycle.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-lifecycle.ts new file mode 100644 index 0000000000000..982741dcb8039 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-lifecycle.ts @@ -0,0 +1,36 @@ +export interface Lifecycle { + LifecycleConfiguration: LifecycleConfiguration; +} + +export interface LifecycleConfiguration { + Rule: Rule[]; +} + +export interface Rule { + ID: string; + Status: string; + Transition: Transition; + Prefix?: string; + Filter?: Filter; +} + +export interface Filter { + And?: And; + Prefix?: string; + Tag?: Tag | Tag[]; +} + +export interface And { + Prefix: string; + Tag: Tag | Tag[]; +} + +export interface Tag { + Key: string; + Value: string; +} + +export interface Transition { + Days: string; + StorageClass: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-lifecycle-list/rgw-bucket-lifecycle-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-lifecycle-list/rgw-bucket-lifecycle-list.component.ts index a48feebe23723..44a88c0ce482d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-lifecycle-list/rgw-bucket-lifecycle-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-lifecycle-list/rgw-bucket-lifecycle-list.component.ts @@ -17,6 +17,7 @@ import { Observable, of } from 'rxjs'; import { catchError, map, tap } from 'rxjs/operators'; import { NotificationService } from '~/app/shared/services/notification.service'; import { NotificationType } from '~/app/shared/enum/notification-type.enum'; +import { Lifecycle, Rule } from '../models/rgw-bucket-lifecycle'; @Component({ selector: 'cd-rgw-bucket-lifecycle-list', @@ -96,7 +97,7 @@ export class RgwBucketLifecycleListComponent implements OnInit { const allLifecycleRules$ = this.rgwBucketService .getLifecycle(this.bucket.bucket, this.bucket.owner) .pipe( - tap((lifecycle) => { + tap((lifecycle: Lifecycle) => { this.lifecycleRuleList = lifecycle; }), catchError(() => { @@ -108,7 +109,7 @@ export class RgwBucketLifecycleListComponent implements OnInit { this.filteredLifecycleRules$ = allLifecycleRules$.pipe( map( (lifecycle: any) => - lifecycle?.LifecycleConfiguration?.Rules?.filter((rule: object) => + lifecycle?.LifecycleConfiguration?.Rule?.filter((rule: Rule) => rule.hasOwnProperty('Transition') ) || [] ) @@ -130,10 +131,10 @@ export class RgwBucketLifecycleListComponent implements OnInit { deleteAction() { const ruleNames = this.selection.selected.map((rule) => rule.ID); - const filteredRules = this.lifecycleRuleList.LifecycleConfiguration.Rules.filter( - (rule: any) => !ruleNames.includes(rule.ID) + const filteredRules = this.lifecycleRuleList.LifecycleConfiguration.Rule.filter( + (rule: Rule) => !ruleNames.includes(rule.ID) ); - const rules = filteredRules.length > 0 ? { Rules: filteredRules } : {}; + const rules = filteredRules.length > 0 ? { Rule: filteredRules } : {}; this.modalRef = this.modalService.show(DeleteConfirmationModalComponent, { itemDescription: $localize`Rule`, itemNames: ruleNames, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-tiering-form/rgw-bucket-tiering-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-tiering-form/rgw-bucket-tiering-form.component.html index e704670e829e1..f3a41fd725318 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-tiering-form/rgw-bucket-tiering-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-tiering-form/rgw-bucket-tiering-form.component.html @@ -136,25 +136,35 @@ class="form-item form-item-append">
Name of the object key + i18n-placeholder + [invalid]="tieringForm.controls['tags'].controls[i].controls['Key'].invalid && tieringForm.controls['tags'].controls[i].controls['Key'].dirty"/> + + This field is required. +
Value of the tag + i18n-placeholder + [invalid]="tieringForm.controls['tags'].controls[i].controls['Value'].invalid && tieringForm.controls['tags'].controls[i].controls['Value'].dirty"/> + + This field is required. +
{ - this.configuredLifecycle = lifecycle || { LifecycleConfiguration: { Rules: [] } }; + .subscribe((lifecycle: Lifecycle) => { + if (lifecycle === null) { + lifecycle = { LifecycleConfiguration: { Rule: [] } }; + } + lifecycle.LifecycleConfiguration.Rule = lifecycle.LifecycleConfiguration.Rule.map( + (rule: Rule) => { + if (rule?.['Filter']?.['Tag'] && !Array.isArray(rule?.['Filter']?.['Tag'])) { + rule['Filter']['Tag'] = [rule['Filter']['Tag']]; + } + if ( + rule?.['Filter']?.['And']?.['Tag'] && + !Array.isArray(rule?.['Filter']?.['And']?.['Tag']) + ) { + rule['Filter']['And']['Tag'] = [rule['Filter']['And']['Tag']]; + } + return rule; + } + ); + this.configuredLifecycle = lifecycle; if (this.editing) { - const ruleToEdit = this.configuredLifecycle?.['LifecycleConfiguration']?.['Rules'].filter( - (rule: any) => rule?.['ID'] === this.selectedLifecycle?.['ID'] + const ruleToEdit = this.configuredLifecycle?.['LifecycleConfiguration']?.['Rule'].filter( + (rule: Rule) => rule?.['ID'] === this.selectedLifecycle?.['ID'] )[0]; this.tieringForm.patchValue({ name: ruleToEdit?.['ID'], @@ -89,13 +107,13 @@ export class RgwBucketTieringFormComponent extends CdForm implements OnInit { this.loadStorageClass(); } - checkIfRuleHasFilters(rule: any) { + checkIfRuleHasFilters(rule: Rule) { if ( this.isValidPrefix(rule?.['Prefix']) || this.isValidPrefix(rule?.['Filter']?.['Prefix']) || - this.isValidArray(rule?.['Filter']?.['Tags']) || + this.isValidArray(rule?.['Filter']?.['Tag']) || this.isValidPrefix(rule?.['Filter']?.['And']?.['Prefix']) || - this.isValidArray(rule?.['Filter']?.['And']?.['Tags']) + this.isValidArray(rule?.['Filter']?.['And']?.['Tag']) ) { return true; } @@ -103,21 +121,21 @@ export class RgwBucketTieringFormComponent extends CdForm implements OnInit { } isValidPrefix(value: string) { - return value !== undefined && value !== ''; + return !!value; } - isValidArray(value: object[]) { + isValidArray(value: Tag | Tag[]) { return Array.isArray(value) && value.length > 0; } - setTags(rule: any) { - if (rule?.['Filter']?.['Tags']?.length > 0) { - rule?.['Filter']?.['Tags']?.forEach((tag: { Key: string; Value: string }) => + setTags(rule: Rule) { + if (Array.isArray(rule?.Filter?.Tag) && rule?.Filter?.Tag?.length > 0) { + rule?.['Filter']?.['Tag']?.forEach((tag: { Key: string; Value: string }) => this.addTags(tag.Key, tag.Value) ); } - if (rule?.['Filter']?.['And']?.['Tags']?.length > 0) { - rule?.['Filter']?.['And']?.['Tags']?.forEach((tag: { Key: string; Value: string }) => + if (Array.isArray(rule?.Filter?.And?.Tag) && rule?.Filter?.And?.Tag?.length > 0) { + rule?.['Filter']?.['And']?.['Tag']?.forEach((tag: { Key: string; Value: string }) => this.addTags(tag.Key, tag.Value) ); } @@ -130,17 +148,17 @@ export class RgwBucketTieringFormComponent extends CdForm implements OnInit { addTags(key?: string, value?: string) { this.tags.push( new FormGroup({ - Key: new FormControl(key), - Value: new FormControl(value) + Key: new FormControl(key || '', Validators.required), + Value: new FormControl(value || '', Validators.required) }) ); this.cd.detectChanges(); } duplicateConfigName(control: AbstractControl): ValidationErrors | null { - if (this.configuredLifecycle?.LifecycleConfiguration?.Rules?.length > 0) { - const ruleIds = this.configuredLifecycle.LifecycleConfiguration.Rules.map( - (rule: any) => rule.ID + if (this.configuredLifecycle?.LifecycleConfiguration?.Rule?.length > 0) { + const ruleIds = this.configuredLifecycle.LifecycleConfiguration.Rule.map( + (rule: Rule) => rule.ID ); return ruleIds.includes(control.value) ? { duplicate: true } : null; } @@ -181,15 +199,13 @@ export class RgwBucketTieringFormComponent extends CdForm implements OnInit { return; } - let lifecycle: any = { + let lifecycle: Rule = { ID: this.tieringForm.getRawValue().name, Status: formValue.status, - Transition: [ - { - Days: formValue.days, - StorageClass: formValue.storageClass - } - ] + Transition: { + Days: formValue.days, + StorageClass: formValue.storageClass + } }; if (formValue.hasPrefix) { if (this.tags.length > 0) { @@ -210,11 +226,11 @@ export class RgwBucketTieringFormComponent extends CdForm implements OnInit { } } else { Object.assign(lifecycle, { - Filter: {} + Prefix: '' }); } if (!this.editing) { - this.configuredLifecycle.LifecycleConfiguration.Rules.push(lifecycle); + this.configuredLifecycle.LifecycleConfiguration.Rule.push(lifecycle); this.rgwBucketService .setLifecycle( this.bucket.bucket, @@ -237,8 +253,10 @@ export class RgwBucketTieringFormComponent extends CdForm implements OnInit { } }); } else { - const rules = this.configuredLifecycle.LifecycleConfiguration.Rules; - const index = rules.findIndex((rule: any) => rule?.['ID'] === this.selectedLifecycle?.['ID']); + const rules = this.configuredLifecycle.LifecycleConfiguration.Rule; + const index = rules.findIndex( + (rule: Rule) => rule?.['ID'] === this.selectedLifecycle?.['ID'] + ); rules.splice(index, 1, lifecycle); this.rgwBucketService .setLifecycle( @@ -266,5 +284,6 @@ export class RgwBucketTieringFormComponent extends CdForm implements OnInit { goToCreateStorageClass() { this.router.navigate(['rgw/tiering/create']); + this.closeModal(); } } diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index f9423da564e7e..4c3c4c07eeda1 100755 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -737,7 +737,16 @@ class RgwClient(RestClient): try: result = request( raw_content=True, headers={'Accept': 'text/xml'}).decode() # type: ignore - return xmltodict.parse(remove_namespace(result), process_namespaces=False) + lifecycle = xmltodict.parse(remove_namespace(result), process_namespaces=False) + if lifecycle is not None: + lifecycle_config = lifecycle.get('LifecycleConfiguration', {}) + rule = lifecycle_config.get('Rule') + + if isinstance(rule, dict): + lifecycle_config['Rule'] = [rule] + + lifecycle['LifecycleConfiguration'] = lifecycle_config + return lifecycle except RequestException as e: if e.content: root = ET.fromstring(e.content)