]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: bucket lifecycle fixes after using xmltodict package 62100/head
authorNaman Munet <naman.munet@ibm.com>
Tue, 4 Mar 2025 10:26:21 +0000 (15:56 +0530)
committerNaman Munet <naman.munet@ibm.com>
Wed, 19 Mar 2025 13:51:25 +0000 (19:21 +0530)
Fixes: https://tracker.ceph.com/issues/70275
Signed-off-by: Naman Munet <naman.munet@ibm.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-lifecycle.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-lifecycle-list/rgw-bucket-lifecycle-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-tiering-form/rgw-bucket-tiering-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-tiering-form/rgw-bucket-tiering-form.component.ts
src/pybind/mgr/dashboard/services/rgw_client.py

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 (file)
index 0000000..982741d
--- /dev/null
@@ -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;
+}
index a48feebe2372373994a9842ef7ddbab0f5dfd4e2..44a88c0ce482d5e586038414d692f0ac4c7bc444 100644 (file)
@@ -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,
index e704670e829e14f4c70f5eca082ce2beeb439430..f3a41fd725318eec8461ea6c1592eff9595dc70c 100644 (file)
                  class="form-item form-item-append">
               <div cdsCol>
                 <cds-text-label labelInputID="Key"
+                                [invalid]="!tieringForm.controls['tags'].controls[i].controls['Key'].valid && tieringForm.controls['tags'].controls[i].controls['Key'].dirty"
+                                [invalidText]="tagKeyError"
                                 i18n>Name of the object key
                   <input cdsText
                          type="text"
                          placeholder="Enter name of the object key"
-                         id="Key"
                          formControlName="Key"
-                         i18n-placeholder/>
+                         i18n-placeholder
+                         [invalid]="tieringForm.controls['tags'].controls[i].controls['Key'].invalid && tieringForm.controls['tags'].controls[i].controls['Key'].dirty"/>
                 </cds-text-label>
+                <ng-template #tagKeyError>
+                  <ng-container i18n>This field is required.</ng-container>
+                </ng-template>
               </div>
               <div cdsCol>
                 <cds-text-label labelInputID="Value"
+                                [invalid]="!tieringForm.controls['tags'].controls[i].controls['Value'].valid && tieringForm.controls['tags'].controls[i].controls['Value'].dirty"
+                                [invalidText]="tagValueError"
                                 i18n>Value of the tag
                   <input cdsText
                          type="text"
                          placeholder="Enter value of the tag"
-                         id="Value"
                          formControlName="Value"
-                         i18n-placeholder/>
+                         i18n-placeholder
+                         [invalid]="tieringForm.controls['tags'].controls[i].controls['Value'].invalid && tieringForm.controls['tags'].controls[i].controls['Value'].dirty"/>
                 </cds-text-label>
+                <ng-template #tagValueError>
+                  <ng-container i18n>This field is required.</ng-container>
+                </ng-template>
               </div>
               <div cdsCol
                    [columnNumbers]="{ lg: 2, md: 2 }"
index 00e4f002b9a6aca36f8ac14760a7e5d452fdce46..db1191dc951cba5dd034e21d81741e92695b0f1c 100644 (file)
@@ -20,6 +20,7 @@ import { StorageClass, ZoneGroupDetails } from '../models/rgw-storage-class.mode
 import { CdForm } from '~/app/shared/forms/cd-form';
 import { Router } from '@angular/router';
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Lifecycle, Rule, Tag } from '../models/rgw-bucket-lifecycle';
 
 export interface Tags {
   tagKey: number;
@@ -35,12 +36,12 @@ export class RgwBucketTieringFormComponent extends CdForm implements OnInit {
   tieringForm: CdFormGroup;
   tagsToRemove: Tags[] = [];
   storageClassList: StorageClass[] = null;
-  configuredLifecycle: any;
+  configuredLifecycle: Lifecycle;
   isStorageClassFetched = false;
 
   constructor(
     @Inject('bucket') public bucket: Bucket,
-    @Optional() @Inject('selectedLifecycle') public selectedLifecycle: any,
+    @Optional() @Inject('selectedLifecycle') public selectedLifecycle: Rule,
     @Optional() @Inject('editing') public editing = false,
     public actionLabels: ActionLabelsI18n,
     private rgwBucketService: RgwBucketService,
@@ -56,11 +57,28 @@ export class RgwBucketTieringFormComponent extends CdForm implements OnInit {
   ngOnInit() {
     this.rgwBucketService
       .getLifecycle(this.bucket.bucket, this.bucket.owner)
-      .subscribe((lifecycle) => {
-        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();
   }
 }
index f9423da564e7ebc7d0015fd19ea9332b62a9ea73..4c3c4c07eeda1774c5007a68e6923278505c4a67 100755 (executable)
@@ -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)