]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard:RGW- Storage Class ACL Mapping 64788/head
authorDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Fri, 1 Aug 2025 04:30:18 +0000 (10:00 +0530)
committerDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Fri, 29 Aug 2025 08:43:36 +0000 (14:13 +0530)
Fixes: https://tracker.ceph.com/issues/72362
Signed-off-by: Dnyaneshwari Talwekar <dtalwekar@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-storage-class.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/rgw-bucket-tiering.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.spec.ts
src/pybind/mgr/dashboard/services/rgw_client.py

index 5fd5ffe34b1387f298acc4ab7d48c9c70d796a5f..7a34f9450653dc6fdc3aa6ce8557b60b6137fbb8 100644 (file)
@@ -48,6 +48,8 @@ export interface StorageClassDetails {
   read_through_restore_days?: number;
   restore_storage_class?: string;
   retain_head_object?: boolean;
+  acls?: ACL[];
+  acl_mappings?: ACL[];
 }
 
 export interface ZoneGroup {
@@ -56,6 +58,24 @@ export interface ZoneGroup {
   placement_targets?: Target[];
 }
 
+export interface ACL {
+  key: string;
+  val: ACLVal;
+}
+
+export interface ACLVal extends AclMapping {
+  type: string;
+}
+
+export interface AclMapping {
+  source_id: string;
+  dest_id: string;
+}
+
+export interface GroupedACLs {
+  [type: string]: AclMapping[];
+}
+
 export interface S3Details {
   endpoint: string;
   access_key: string;
@@ -69,6 +89,7 @@ export interface S3Details {
   host_style: boolean;
   retain_head_object?: boolean;
   allow_read_through?: boolean;
+  acl_mappings?: ACL[];
 }
 export interface S3Glacier {
   glacier_restore_days: number;
@@ -84,6 +105,7 @@ export interface PlacementTarget {
   placement_id: string;
   tags?: string[];
   tier_type?: TIER_TYPE;
+  tier_config_rm: TierConfigRm;
   tier_config?: {
     endpoint: string;
     access_key: string;
@@ -99,13 +121,18 @@ export interface PlacementTarget {
     restore_storage_class?: string;
     read_through_restore_days?: number;
     target_storage_class?: string;
+    acls?: ACL[];
   };
   storage_class?: string;
   name?: string;
   tier_targets?: TierTarget[];
 }
 
-export interface StorageClassOption {
+export interface TierConfigRm {
+  [key: string]: string;
+}
+
+export interface TypeOption {
   value: string;
   label: string;
 }
@@ -171,35 +198,27 @@ export const TIER_TYPE_DISPLAY = {
   GLACIER: 'Cloud S3 Glacier'
 };
 
-export const GLACIER_TARGET_STORAGE_CLASS = 'GLACIER';
+export const GLACIER_TARGET_STORAGE_CLASS = $localize`GLACIER`;
 
-export const ALLOW_READ_THROUGH_TEXT =
-  'Enables fetching objects from remote cloud S3 if not found locally.';
+export const ALLOW_READ_THROUGH_TEXT = $localize`Enables fetching objects from remote cloud S3 if not found locally.`;
 
-export const MULTIPART_MIN_PART_TEXT =
-  'It specifies that objects this size or larger are transitioned to the cloud using multipart upload.';
+export const MULTIPART_MIN_PART_TEXT = $localize`It specifies that objects this size or larger are transitioned to the cloud using multipart upload.`;
 
-export const MULTIPART_SYNC_THRESHOLD_TEXT =
-  'It specifies the minimum part size to use when transitioning objects using multipart upload.';
+export const MULTIPART_SYNC_THRESHOLD_TEXT = $localize`It specifies the minimum part size to use when transitioning objects using multipart upload.`;
 
-export const TARGET_PATH_TEXT =
-  'Target Path refers to the storage location (e.g., bucket or container) in the cloud where data will be stored.';
+export const TARGET_PATH_TEXT = $localize`Target Path refers to the storage location (e.g., bucket or container) in the cloud where data will be stored.`;
 
-export const TARGET_REGION_TEXT =
-  'The region of the remote cloud service where storage is located.';
+export const TARGET_REGION_TEXT = $localize`The region of the remote cloud service where storage is located.`;
 
-export const TARGET_ENDPOINT_TEXT =
-  'The URL endpoint of the remote cloud service for accessing storage.';
+export const TARGET_ENDPOINT_TEXT = $localize`The URL endpoint of the remote cloud service for accessing storage.`;
 
-export const TARGET_ACCESS_KEY_TEXT =
-  "To view or copy your access key, go to your cloud service's user management or credentials section, find your user profile, and locate the access key. You can view and copy the key by following the instructions provided.";
+export const TARGET_ACCESS_KEY_TEXT = $localize`To view or copy your access key, go to your cloud service's user management or credentials section, find your user profile, and locate the access key. You can view and copy the key by following the instructions provided.`;
 
-export const TARGET_SECRET_KEY_TEXT =
-  "To view or copy your secret key, go to your cloud service's user management or credentials section, find your user profile, and locate the secret key. You can view and copy the key by following the instructions provided.";
+export const TARGET_SECRET_KEY_TEXT = $localize`To view or copy your secret key, go to your cloud service's user management or credentials section, find your user profile, and locate the secret key. You can view and copy the key by following the instructions provided.`;
 
-export const RETAIN_HEAD_OBJECT_TEXT = 'Retain object metadata after transition to the cloud.';
+export const RETAIN_HEAD_OBJECT_TEXT = $localize`Retain object metadata after transition to the cloud.`;
 
-export const HOST_STYLE = `The URL format for accessing the remote S3 endpoint:
+export const HOST_STYLE = $localize`The URL format for accessing the remote S3 endpoint:
   - 'Path': Use for a path-based URL
   - 'Virtual': Use for a domain-based URL`;
 
@@ -226,3 +245,68 @@ export const RESTORE_STORAGE_CLASS_TEXT = $localize`The storage class to which o
 export const ZONEGROUP_TEXT = $localize`A Zone Group is a logical grouping of one or more zones that share the same data
                   and metadata, allowing for multi-site replication and geographic distribution of
                   data.`;
+
+export type AclType = 'id' | 'email' | 'uri';
+
+export interface AclLabelAndHelper {
+  source: string;
+  destination: string;
+}
+
+export interface AclMaps {
+  [key: string]: AclLabelAndHelper & {
+    [field: string]: string;
+  };
+}
+
+export enum AclLabel {
+  source = 'Source',
+  destination = 'Destination'
+}
+
+export enum AclFieldType {
+  Source = 'source',
+  Destination = 'destination'
+}
+
+export const AclTypeOptions = [
+  { value: 'id', label: 'ID' },
+  { value: 'email', label: 'Email' },
+  { value: 'uri', label: 'URI' }
+] as const;
+
+export const AclTypeConst = {
+  ID: 'id',
+  EMAIL: 'email',
+  URI: 'uri'
+} as const;
+
+export const AclTypeLabel: AclMaps = {
+  id: {
+    source: $localize`Source User`,
+    destination: $localize`Destination User`
+  },
+  email: {
+    source: $localize`Source Email`,
+    destination: $localize`Destination Email`
+  },
+  uri: {
+    source: $localize`Source URI`,
+    destination: $localize`Destination URI`
+  }
+};
+
+export const AclHelperText: AclMaps = {
+  id: {
+    source: $localize`The unique user ID in the source system.`,
+    destination: $localize`The unique user ID in the destination system.`
+  },
+  email: {
+    source: $localize`The email address of the source user.`,
+    destination: $localize`The email address of the destination user.`
+  },
+  uri: {
+    source: $localize`The URI identifying the source group or user.`,
+    destination: $localize`The URI identifying the destination group or user.`
+  }
+};
index dc8886ef5aa6b0173cf2bdfb993ec161699ef91f..701ae11f27fd87920986658326c4c82bff16d130 100644 (file)
@@ -7,9 +7,7 @@
         data-testid="rgw-storage-details"
       >
         <tbody>
-          @if( isTierMatch(
-            TIER_TYPE_DISPLAY.LOCAL
-          )){
+          @if( isTierMatch( TIER_TYPE_DISPLAY.LOCAL )){
           <tr>
             <td class="bold"
                 i18n>
             </td>
             <td>{{ selection?.zonegroup_name }}</td>
           </tr>
-        }
-        @if(isTierMatch(
-            TIER_TYPE_DISPLAY.LOCAL
-          )){
+          } @if(isTierMatch( TIER_TYPE_DISPLAY.LOCAL )){
           <tr>
             <td class="bold"
                 i18n>
@@ -39,8 +34,7 @@
             </td>
             <td>{{ selection?.placement_target }}</td>
           </tr>
-        }
-        @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
+          } @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
           <tr>
             <td class="bold"
                 i18n>
@@ -53,8 +47,6 @@
             </td>
             <td>{{ selection?.target_path }}</td>
           </tr>
-        }
-        @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
           <tr>
             <td class="bold"
                 i18n>
@@ -83,8 +75,6 @@
               </div>
             </td>
           </tr>
-        }
-        @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
           <tr>
             <td class="bold"
                 i18n>
               </div>
             </td>
           </tr>
-        }
-        @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
           <tr>
             <td class="bold"
                 i18n>
             </td>
             <td>{{ selection?.host_style }}</td>
           </tr>
-        }
-        @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
           <tr>
             <td class="bold"
                 i18n>
             </td>
             <td>{{ selection?.retain_head_object ? 'Enabled' : 'Disabled' }}</td>
           </tr>
-        }
-        @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
           <tr>
             <td class="bold"
                 i18n>
             </td>
             <td>{{ selection?.allow_read_through ? 'Enabled' : 'Disabled' }}</td>
           </tr>
-        }
-        @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER) && (selection?.allow_read_through)) {
-          <tr *ngIf="isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER) && (selection?.allow_read_through)">
+          } @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER) &&
+          (selection?.allow_read_through)) {
+          <tr>
             <td class="bold"
                 i18n>
               Read through Restore Days
                 </span>
               </cd-helper>
             </td>
-            <td>{{ selection?.read_through_restore_days }}</td>
+            <td>
+              {{ selection?.read_through_restore_days }}
+              {{ selection?.read_through_restore_days === 1 ? 'Day' : 'Days' }}
+            </td>
           </tr>
-        }
-        @if(isTierMatch( TIER_TYPE_DISPLAY.GLACIER)){
+          } @if(isTierMatch( TIER_TYPE_DISPLAY.GLACIER)){
           <tr>
             <td class="bold"
                 i18n>
                 </span>
               </cd-helper>
             </td>
-            <td>{{ selection?.glacier_restore_days }}</td>
+            <td>  {{ selection?.glacier_restore_days }}
+              {{ selection?.glacier_restore_days === 1 ? 'Day' : 'Days' }}</td>
           </tr>
-        }
-        @if(isTierMatch( TIER_TYPE_DISPLAY.GLACIER)) {
           <tr>
             <td class="bold"
                 i18n>
             </td>
             <td>{{ selection?.glacier_restore_tier_type }}</td>
           </tr>
-        }
-        @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
+          } @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
           <tr>
             <td class="bold"
                 i18n>
             </td>
             <td>{{ selection?.restore_storage_class }}</td>
           </tr>
-        }
-          @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
           <tr>
             <td class="bold"
                 i18n>
             </td>
             <td>{{ selection?.multipart_min_part_size | dimlessBinary }}</td>
           </tr>
-        }
-        @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){
           <tr>
             <td class="bold"
                 i18n>
               Multipart Sync Threshold
               <cd-helper class="text-pre-wrap">
                 <span>
-                  {{ multipartSyncThreholdText  }}
+                  {{ multipartSyncThreholdText }}
                 </span>
               </cd-helper>
             </td>
             <td>{{ selection?.multipart_sync_threshold | dimlessBinary }}</td>
           </tr>
-        }
+          } @if(selection?.acl_mappings.length > 0) {
+          <tr>
+            <td class="bold w-25"
+                i18n>ACLs</td>
+            <td>
+              <ng-container *ngFor="let type of groupedACLs | keyvalue">
+                <div class="mb-2">
+                  <div class="cds--label">{{ type.key }}:</div>
+                  <div *ngFor="let item of type.value"
+                       [cdsStack]="'horizontal'"
+                       class="mt-1">
+                    <cds-tag size="sm"
+                             class="tags-background-gray">
+                      {{ item.source_id }} : {{ item.dest_id }}
+                    </cds-tag>
+                  </div>
+                </div>
+              </ng-container>
+            </td>
+          </tr>
+         }
         </tbody>
       </table>
     </cds-tab>
index 6275c62d485c0cebc1a1f388e321d597bad73950..1321a72c035a75d2a933d32e7f63a3bed6401730 100644 (file)
@@ -1,16 +1,29 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
-
 import { RgwStorageClassDetailsComponent } from './rgw-storage-class-details.component';
 import { StorageClassDetails } from '../models/rgw-storage-class.model';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { SharedModule } from '~/app/shared/shared.module';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { RouterTestingModule } from '@angular/router/testing';
+import { SimpleChange } from '@angular/core';
 
 describe('RgwStorageClassDetailsComponent', () => {
   let component: RgwStorageClassDetailsComponent;
   let fixture: ComponentFixture<RgwStorageClassDetailsComponent>;
 
+  const mockSelection: StorageClassDetails = {
+    access_key: 'TestAccessKey',
+    secret: 'TestSecret',
+    target_path: '/test/path',
+    multipart_min_part_size: 100,
+    multipart_sync_threshold: 200,
+    host_style: 'path',
+    retain_head_object: true,
+    allow_read_through: true,
+    tier_type: 'local',
+    acl_mappings: []
+  };
+
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       imports: [
@@ -24,6 +37,8 @@ describe('RgwStorageClassDetailsComponent', () => {
 
     fixture = TestBed.createComponent(RgwStorageClassDetailsComponent);
     component = fixture.componentInstance;
+    component.selection = mockSelection;
+
     fixture.detectChanges();
   });
 
@@ -32,19 +47,28 @@ describe('RgwStorageClassDetailsComponent', () => {
   });
 
   it('should update storageDetails when selection input changes', () => {
-    const mockSelection: StorageClassDetails = {
-      access_key: 'TestAccessKey',
-      secret: 'TestSecret',
-      target_path: '/test/path',
-      multipart_min_part_size: 100,
-      multipart_sync_threshold: 200,
-      host_style: 'path',
-      retain_head_object: true,
-      allow_read_through: true,
-      tier_type: 'local'
+    const newSelection: StorageClassDetails = {
+      access_key: 'NewAccessKey',
+      secret: 'NewSecret',
+      target_path: '/new/path',
+      multipart_min_part_size: 500,
+      multipart_sync_threshold: 1000,
+      host_style: 'virtual',
+      retain_head_object: false,
+      allow_read_through: false,
+      tier_type: 'archive',
+      glacier_restore_days: 1,
+      glacier_restore_tier_type: 'standard',
+      placement_targets: '',
+      read_through_restore_days: 7,
+      restore_storage_class: 'restored',
+      zonegroup_name: 'zone1'
     };
-    component.selection = mockSelection;
-    component.ngOnChanges();
-    expect(component.storageDetails).toEqual(mockSelection);
+
+    component.selection = newSelection;
+    component.ngOnChanges({
+      selection: new SimpleChange(null, newSelection, false)
+    });
+    expect(component.storageDetails).toEqual(newSelection);
   });
 });
index 40049a69b959fdf1258a393c9e8c16288a00c8c4..69c8f9dbfe88a5a37bf825ab8abefeedb0b12df3 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, Input, OnChanges } from '@angular/core';
+import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
 import {
   ALLOW_READ_THROUGH_TEXT,
@@ -17,14 +17,16 @@ import {
   RESTORE_DAYS_TEXT,
   READTHROUGH_RESTORE_DAYS_TEXT,
   RESTORE_STORAGE_CLASS_TEXT,
-  ZONEGROUP_TEXT
+  ZONEGROUP_TEXT,
+  ACL,
+  GroupedACLs
 } from '../models/rgw-storage-class.model';
 @Component({
   selector: 'cd-rgw-storage-class-details',
   templateUrl: './rgw-storage-class-details.component.html',
   styleUrls: ['./rgw-storage-class-details.component.scss']
 })
-export class RgwStorageClassDetailsComponent implements OnChanges {
+export class RgwStorageClassDetailsComponent implements OnChanges, OnInit {
   @Input()
   selection: StorageClassDetails;
   columns: CdTableColumn[] = [];
@@ -45,31 +47,48 @@ export class RgwStorageClassDetailsComponent implements OnChanges {
   readthroughrestoreDaysText = READTHROUGH_RESTORE_DAYS_TEXT;
   restoreStorageClassText = RESTORE_STORAGE_CLASS_TEXT;
   zoneGroupText = ZONEGROUP_TEXT;
+  groupedACLs: GroupedACLs = {};
 
-  ngOnChanges() {
-    if (this.selection) {
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes['selection']) {
       this.storageDetails = {
-        zonegroup_name: this.selection.zonegroup_name,
-        placement_targets: this.selection.placement_targets,
-        access_key: this.selection.access_key,
-        secret: this.selection.secret,
-        target_path: this.selection.target_path,
-        tier_type: this.selection.tier_type,
-        multipart_min_part_size: this.selection.multipart_min_part_size,
-        multipart_sync_threshold: this.selection.multipart_sync_threshold,
-        host_style: this.selection.host_style,
-        retain_head_object: this.selection.retain_head_object,
-        allow_read_through: this.selection.allow_read_through,
-        glacier_restore_days: this.selection.glacier_restore_days,
-        glacier_restore_tier_type: this.selection.glacier_restore_tier_type,
-        restore_storage_class: this.selection.restore_storage_class,
-        read_through_restore_days: this.selection.read_through_restore_days
+        zonegroup_name: this.selection?.zonegroup_name,
+        placement_targets: this.selection?.placement_targets,
+        access_key: this.selection?.access_key,
+        secret: this.selection?.secret,
+        target_path: this.selection?.target_path,
+        tier_type: this.selection?.tier_type,
+        multipart_min_part_size: this.selection?.multipart_min_part_size,
+        multipart_sync_threshold: this.selection?.multipart_sync_threshold,
+        host_style: this.selection?.host_style,
+        retain_head_object: this.selection?.retain_head_object,
+        allow_read_through: this.selection?.allow_read_through,
+        glacier_restore_days: this.selection?.glacier_restore_days,
+        glacier_restore_tier_type: this.selection?.glacier_restore_tier_type,
+        restore_storage_class: this.selection?.restore_storage_class,
+        read_through_restore_days: this.selection?.read_through_restore_days
       };
     }
   }
 
+  ngOnInit() {
+    this.groupedACLs = this.groupByType(this.selection.acl_mappings);
+  }
+
   isTierMatch(...types: string[]): boolean {
     const tier_type = this.selection.tier_type?.toLowerCase();
     return types.some((type) => type.toLowerCase() === tier_type);
   }
+
+  groupByType(acls: ACL[]): GroupedACLs {
+    return acls?.reduce((groupAcls: GroupedACLs, item: ACL) => {
+      const type = item.val?.type?.toUpperCase();
+      groupAcls[type] = groupAcls[type] ?? [];
+      groupAcls[type].push({
+        source_id: item.val?.source_id,
+        dest_id: item.val?.dest_id
+      });
+      return groupAcls;
+    }, {});
+  }
 }
index 57076071a5cb920a09630b32091a172f8ffc9cb7..138bc4131ecf245aad83ddcb70406f24ed983e0e 100644 (file)
@@ -80,7 +80,7 @@
             [invalid]="storageClassForm.showError('placement_target', formDir, 'required')"
             [invalidText]="placementError"
           >
-            <option [value]=""
+            <option [value]="''"
                     i18n>--Select--</option>
             <option
               *ngFor="let placementTarget of placementTargets"
           >Name
           <input
             cdsText
-            type="type"
+            type="text"
             id="storage_class"
             formControlName="storage_class"
             [invalid]="storageClassForm.showError('storage_class', formDir, 'required')"
           <div cdsCol>
             <!-- Target Endpoint -->
             <cds-text-label
-            labelInputID="target_endpoint"
-            i18n
-            [invalid]="storageClassForm.showError('target_endpoint', formDir, 'invalidURL') || storageClassForm.showError('target_endpoint', formDir, 'required')"
-            [invalidText]="endpointError"
-            [helperText]="helpTextLabels.targetEndpointText"
-            >Target Endpoint
-            <input
-              cdsText
-              type="text"
-              placeholder="e.g, http://ceph-node-00.com:80"
-              i18n-placeholder
-              id="target_endpoint"
-              formControlName="target_endpoint"
-              [invalid]="storageClassForm.showError('target_endpoint', formDir, 'invalidURL') || storageClassForm.showError('target_endpoint', formDir, 'required')"
-              [invalidText]="endpointError"
-            />
-          </cds-text-label>
-            <ng-template #endpointError>
-              <span
-                class="invalid-feedback"
-                *ngIf="storageClassForm.showError('target_endpoint', formDir, 'required')"
-                i18n
-                >This field is required.</span
-              >
-              <span class="invalid-feedback"
-                    *ngIf="storageClassForm.showError('target_endpoint', formDir, 'invalidURL')"
-                    i18n>Please enter a valid URL.</span>
-            </ng-template>
+              i18n
+              [invalid]="storageClassForm.showError('target_endpoint', formDir, 'required') || storageClassForm.showError('target_endpoint', formDir, 'invalidUrl')"
+
+              [invalidText]="
+                            storageClassForm.controls['target_endpoint'].errors?.['required'] ? 'This field is required.' :
+                            storageClassForm.controls['target_endpoint'].errors?.['invalidUrl'] ? 'Please enter a valid URL.' : ''
+                          "
+              i18n-invalidText
+              >Target Endpoint
+              <input
+                cdsText
+                formControlName="target_endpoint"
+                placeholder="e.g. 192.168.0.10, 192.168.1.0/8"
+                 [invalid]="storageClassForm.showError('target_endpoint', formDir, 'required') || storageClassForm.showError('target_endpoint', formDir, 'invalidUrl')"
+              />
+            </cds-text-label>
           </div>
         </div>
         <!-- Access Key  -->
               [helperText]="helpTextLabels.restoreStorageClassText"
               i18n-label
             >
-              <option value=""
+              <option [value]="''"
                       i18n>-- Select the glacier restore storage class --</option>
               <option [ngValue]="standard"
                       i18n>Standard</option>
               [helperText]="helpTextLabels.tiertypeText"
               i18n-label
             >
-              <option value=""
+              <option value="''"
                       i18n>-- Select the glacier restore tier type --</option>
               <option [ngValue]="standard"
                       i18n>Standard</option>
                     </cds-text-label>
                   </div>
                 </div>
+                <!-- ACLs -->
+                <div>
+                  <legend class="cd-header"
+                          i18n>ACLs Mapping</legend>
+                  <ng-container formArrayName="acls">
+                    @for (acl of acls.controls; let i = $index; track acl) {
+                    <ng-container [formGroupName]="i">
+                      <div class="form-item form-item-append"
+                           cdsRow>
+                        <div cdsCol
+                             [columnNumbers]="{ lg: 7 }">
+                          <cds-select id="type"
+                                      formControlName="type"
+                                      i18n-label
+                                      label="Type">
+                            @for (type of typeOptions; track type.value) {
+                            <option [value]="type.value"
+                                    i18n>
+                              {{ type.label }}
+                            </option>
+                            }
+                          </cds-select>
+                        </div>
+                      </div>
+                      <div cdsRow
+                           class="form-item form-item-append">
+                        <div cdsCol>
+                          <cds-text-label
+                            labelInputID="source_id"
+                            i18n
+                            [helperText]="getAclHelperText(acl.get('type')?.value, 'source')"
+                            [invalid]="
+                              acl.get('source_id')?.invalid && acl.get('source_id')?.touched
+                            "
+                            [invalidText]="
+                            acl.get('source_id')?.errors?.['required'] ? 'This field is required.' :
+                            acl.get('source_id')?.errors?.['email'] ? 'Please enter a valid email address.' :
+                            acl.get('source_id')?.errors?.['invalidUrl'] ? 'Please enter a valid URL.' : ''
+                          "
+                          >
+                            {{ getAclLabel('source', acl.get('type')?.value) }}
+                            <input
+                              cdsText
+                              type="text"
+                              formControlName="source_id"
+                              [invalidText]="
+                            acl.get('source_id')?.errors?.['required'] ? 'This field is required.' :
+                            acl.get('source_id')?.errors?.['email'] ? 'Please enter a valid email address.' :
+                            acl.get('source_id')?.errors?.['invalidUrl'] ? 'Please enter a valid URL.' : ''
+                          "
+                            />
+                          </cds-text-label>
+                        </div>
+                        <div cdsCol>
+                          <cds-text-label
+                            labelInputID="dest_id"
+                            i18n
+                            [helperText]="getAclHelperText(acl.get('type')?.value, 'destination')"
+                            [invalid]="acl.get('dest_id')?.invalid && acl.get('dest_id')?.touched"
+                            [invalidText]="
+                              acl.get('dest_id')?.errors?.['required'] ? 'This field is required.' :
+                              acl.get('dest_id')?.errors?.['email'] ? 'Please enter a valid email address.' :
+                              acl.get('dest_id')?.errors?.['invalidUrl'] ? 'Please enter a valid URL.' : ''
+                            "
+                          >
+                            {{ getAclLabel('destination', acl.get('type')?.value) }}
+                            <input
+                              cdsText
+                              type="text"
+                              formControlName="dest_id"
+                              [invalidText]="
+                              acl.get('dest_id')?.errors?.['required'] ? 'This field is required.' :
+                              acl.get('dest_id')?.errors?.['email'] ? 'Please enter a valid email address.' :
+                              acl.get('dest_id')?.errors?.['invalidUrl'] ? 'Please enter a valid URL.' : ''
+                            "
+                            />
+                          </cds-text-label>
+                        </div>
+
+                        <div
+                          cdsCol
+                          [columnNumbers]="{ lg: 1, md: 1 }"
+                          class="item-action-btn spacing"
+                        >
+                          <cds-icon-button kind="primary"
+                                           size="sm"
+                                           (click)="addAcls(acls, i)">
+                            <svg cdsIcon="add"
+                                 size="32"
+                                 class="cds--btn__icon"></svg>
+                          </cds-icon-button>
+                        </div>
+                        <div cdsCol
+                             [columnNumbers]="{ lg: 1, md: 1 }"
+                             class="item-action-btn">
+                          <cds-icon-button kind="danger"
+                                           size="sm"
+                                           (click)="removeAcl(i)">
+                            <svg cdsIcon="trash-can"
+                                 size="32"
+                                 class="cds--btn__icon"></svg>
+                          </cds-icon-button>
+                        </div>
+                      </div>
+                    </ng-container>
+                    }
+                  </ng-container>
+                </div>
               </cds-accordion-item>
             </cds-accordion>
           </fieldset>
index 4a1c7589ca568cd7f0da0d066bba0b31c44fafc3..4a81ef203c3f31f128a11d25a8e5a4c7976bb1bc 100644 (file)
@@ -1,5 +1,11 @@
-import { Component, OnInit } from '@angular/core';
-import { AbstractControl, FormControl, Validators } from '@angular/forms';
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import {
+  AbstractControl,
+  FormArray,
+  FormControl,
+  ValidationErrors,
+  Validators
+} from '@angular/forms';
 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
 import { CdForm } from '~/app/shared/forms/cd-form';
 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
@@ -8,6 +14,7 @@ import _ from 'lodash';
 import { ActivatedRoute, Router } from '@angular/router';
 import { RgwStorageClassService } from '~/app/shared/api/rgw-storage-class.service';
 import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+
 import {
   ALLOW_READ_THROUGH_TEXT,
   DEFAULT_PLACEMENT,
@@ -37,19 +44,30 @@ import {
   RESTORE_STORAGE_CLASS_TEXT,
   TIER_TYPE_DISPLAY,
   S3Glacier,
-  StorageClassOption,
+  TypeOption,
   STORAGE_CLASS_CONSTANTS,
   STANDARD_TIER_TYPE_TEXT,
   EXPEDITED_TIER_TYPE_TEXT,
   TextLabels,
   CLOUD_TIER_REQUIRED_FIELDS,
   GLACIER_REQUIRED_FIELDS,
-  GLACIER_TARGET_STORAGE_CLASS
+  GLACIER_TARGET_STORAGE_CLASS,
+  AclHelperText,
+  AclTypeLabel,
+  AclFieldType,
+  TierConfigRm,
+  ACL,
+  AclTypeOptions,
+  AclTypeConst,
+  ACLVal,
+  AclLabel,
+  AclType
 } from '../models/rgw-storage-class.model';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 import { NotificationService } from '~/app/shared/services/notification.service';
 import { CdValidators } from '~/app/shared/forms/cd-validators';
 import { FormatterService } from '~/app/shared/services/formatter.service';
+import validator from 'validator';
 
 @Component({
   selector: 'cd-rgw-storage-class-form',
@@ -74,9 +92,17 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
   allowReadThrough: boolean = false;
   TIER_TYPE = TIER_TYPE;
   TIER_TYPE_DISPLAY = TIER_TYPE_DISPLAY;
-  storageClassOptions: StorageClassOption[];
+  storageClassOptions: TypeOption[];
   helpTextLabels: TextLabels;
-
+  typeOptions: TypeOption[];
+  aclTypeLabel = AclTypeLabel;
+  aclHelperText = AclHelperText;
+  aclList: ACL[] = [];
+  removedAclSourceIds: string[] = [];
+  urlValidator = (control: AbstractControl): ValidationErrors | null => {
+    const value = control.value;
+    return !value || validator.isURL(value) ? null : { invalidUrl: true };
+  };
   constructor(
     public actionLabels: ActionLabelsI18n,
     private formBuilder: CdFormBuilder,
@@ -85,7 +111,8 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     private rgwZoneGroupService: RgwZonegroupService,
     private router: Router,
     private route: ActivatedRoute,
-    public formatter: FormatterService
+    public formatter: FormatterService,
+    private cdRef: ChangeDetectorRef
   ) {
     super();
     this.resource = $localize`Tiering Storage Class`;
@@ -117,6 +144,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       { value: TIER_TYPE.CLOUD_TIER, label: TIER_TYPE_DISPLAY.CLOUD_TIER },
       { value: TIER_TYPE.GLACIER, label: TIER_TYPE_DISPLAY.GLACIER }
     ];
+    this.typeOptions = [...AclTypeOptions];
     this.createForm();
     this.storageClassTypeText();
     this.updateTierTypeHelpText();
@@ -134,6 +162,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
             this.storageClassInfo.storage_class
           );
           let response = this.tierTargetInfo?.val?.s3;
+          const aclMappings = this.tierTargetInfo?.val?.s3?.acl_mappings || [];
           this.storageClassForm.get('zonegroup').disable();
           this.storageClassForm.get('placement_target').disable();
           this.storageClassForm.get('storage_class').disable();
@@ -143,6 +172,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
           ) {
             this.storageClassForm.get('storageClassType').disable();
           }
+          this.aclList = this.tierTargetInfo?.val?.s3?.acl_mappings || [];
           this.storageClassForm.patchValue({
             zonegroup: this.storageClassInfo?.zonegroup_name,
             region: response?.region,
@@ -158,8 +188,23 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
             multipart_min_part_size: response?.multipart_min_part_size || '',
             allow_read_through: this.tierTargetInfo?.val?.allow_read_through || false,
             restore_storage_class: this.tierTargetInfo?.val?.restore_storage_class,
-            read_through_restore_days: this.tierTargetInfo?.val?.read_through_restore_days
+            read_through_restore_days: this.tierTargetInfo?.val?.read_through_restore_days,
+            acl_mappings: this.tierTargetInfo?.val?.s3?.acl_mappings || []
           });
+          this.acls?.clear();
+          if (aclMappings.length > 0) {
+            aclMappings.forEach((acl) => {
+              this.acls?.push(
+                this.formBuilder.group({
+                  source_id: [acl.val?.source_id || ''],
+                  dest_id: [acl.val?.dest_id || ''],
+                  type: [acl.val?.type || AclTypeConst.ID, Validators.required]
+                })
+              );
+            });
+          } else {
+            this.addAcls();
+          }
           if (this.tierTargetInfo?.val?.tier_type == TIER_TYPE.GLACIER) {
             let glacierResponse = this.tierTargetInfo?.val['s3-glacier'];
             this.storageClassForm.patchValue({
@@ -177,57 +222,6 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     });
   }
 
-  private updateValidatorsBasedOnStorageClass(value: string) {
-    GLACIER_REQUIRED_FIELDS.forEach((field) => {
-      const control = this.storageClassForm.get(field);
-
-      if (
-        (value === TIER_TYPE.CLOUD_TIER && CLOUD_TIER_REQUIRED_FIELDS.includes(field)) ||
-        (value === TIER_TYPE.GLACIER && GLACIER_REQUIRED_FIELDS.includes(field))
-      ) {
-        control.setValidators([Validators.required]);
-      } else {
-        control.clearValidators();
-      }
-      control.updateValueAndValidity();
-    });
-
-    if (this.editing) {
-      const defaultValues = {
-        allow_read_through: false,
-        read_through_restore_days: STORAGE_CLASS_CONSTANTS.DEFAULT_READTHROUGH_RESTORE_DAYS,
-        restore_storage_class: STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS,
-        multipart_min_part_size: STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE,
-        multipart_sync_threshold: STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_SYNC_THRESHOLD
-      };
-      Object.keys(defaultValues).forEach((key) => {
-        this.storageClassForm.get(key).setValue(defaultValues[key]);
-      });
-    }
-  }
-
-  storageClassTypeText() {
-    this.storageClassForm?.get('storageClassType')?.valueChanges.subscribe((value) => {
-      if (value === TIER_TYPE.LOCAL) {
-        this.helpTextLabels.storageClassText = LOCAL_STORAGE_CLASS_TEXT;
-      } else if (value === TIER_TYPE.CLOUD_TIER) {
-        this.helpTextLabels.storageClassText = CLOUDS3_STORAGE_CLASS_TEXT;
-      } else if (value === TIER_TYPE.GLACIER) {
-        this.helpTextLabels.storageClassText = GLACIER_STORAGE_CLASS_TEXT;
-      }
-    });
-  }
-
-  updateTierTypeHelpText() {
-    this.storageClassForm?.get('glacier_restore_tier_type')?.valueChanges.subscribe((value) => {
-      if (value === STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS) {
-        this.helpTextLabels.tiertypeText = STANDARD_TIER_TYPE_TEXT;
-      } else {
-        this.helpTextLabels.tiertypeText = EXPEDITED_TIER_TYPE_TEXT;
-      }
-    });
-  }
-
   createForm() {
     const self = this;
 
@@ -252,9 +246,6 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       placement_target: new FormControl('', {
         validators: [Validators.required]
       }),
-      target_endpoint: new FormControl(null, {
-        validators: [CdValidators.url, Validators.required]
-      }),
       access_key: new FormControl(null, [
         CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
       ]),
@@ -268,6 +259,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       glacier_restore_tier_type: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS, [
         CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER }, [Validators.required])
       ]),
+      target_endpoint: new FormControl('', [Validators.required, this.urlValidator]),
       glacier_restore_days: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_GLACIER_RESTORE_DAYS, [
         CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER || TIER_TYPE.CLOUD_TIER }, [
           CdValidators.number(false),
@@ -295,7 +287,143 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
         STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE
       ),
       allow_read_through: new FormControl(false),
-      storageClassType: new FormControl(TIER_TYPE.LOCAL, Validators.required)
+      storageClassType: new FormControl(TIER_TYPE.LOCAL, Validators.required),
+      acls: new FormArray([this.createAcls()])
+    });
+  }
+
+  public createAcls(): CdFormGroup {
+    const group = this.formBuilder.group({
+      type: new FormControl(AclTypeConst.ID, Validators.required),
+      source_id: new FormControl(''),
+      dest_id: new FormControl('')
+    });
+
+    const sourceId = group.get('source_id');
+    const destId = group.get('dest_id');
+
+    const validators = this.getValidatorsType(AclTypeConst.ID);
+
+    sourceId.setValidators(validators);
+    destId.setValidators(validators);
+
+    sourceId.updateValueAndValidity();
+    destId.updateValueAndValidity();
+
+    group.get('type')?.valueChanges.subscribe((newType: AclType) => {
+      const sourceId = group.get('source_id');
+      const destId = group.get('dest_id');
+
+      const validators = this.getValidatorsType(newType);
+
+      sourceId.setValidators(validators);
+      destId.setValidators(validators);
+
+      sourceId.updateValueAndValidity();
+      destId.updateValueAndValidity();
+    });
+
+    return group;
+  }
+
+  private getValidatorsType(type: AclType) {
+    switch (type) {
+      case AclTypeConst.EMAIL:
+        return [Validators.email];
+      case AclTypeConst.URI:
+        return [this.urlValidator];
+      case AclTypeConst.ID:
+      default:
+        return [Validators.required];
+    }
+  }
+
+  get acls(): FormArray {
+    return this.storageClassForm.get('acls') as FormArray;
+  }
+
+  private updateValidatorsBasedOnStorageClass(value: string) {
+    GLACIER_REQUIRED_FIELDS.forEach((field) => {
+      const control = this.storageClassForm.get(field);
+
+      if (
+        (value === TIER_TYPE.CLOUD_TIER && CLOUD_TIER_REQUIRED_FIELDS.includes(field)) ||
+        (value === TIER_TYPE.GLACIER && GLACIER_REQUIRED_FIELDS.includes(field))
+      ) {
+        control.setValidators([Validators.required]);
+      } else {
+        control.clearValidators();
+      }
+      control.updateValueAndValidity();
+    });
+
+    if (this.editing) {
+      const defaultValues = {
+        allow_read_through: false,
+        read_through_restore_days: STORAGE_CLASS_CONSTANTS.DEFAULT_READTHROUGH_RESTORE_DAYS,
+        restore_storage_class: STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS,
+        multipart_min_part_size: STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE,
+        multipart_sync_threshold: STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_SYNC_THRESHOLD
+      };
+      Object.keys(defaultValues).forEach((key) => {
+        this.storageClassForm.get(key).setValue(defaultValues[key]);
+      });
+    }
+  }
+
+  addAcls() {
+    this.acls.push(this.createAcls());
+  }
+
+  removeAcl(index: number) {
+    if (this.acls.length > 1) {
+      this.acls.removeAt(index);
+    } else {
+      const removedAcl = this.acls.at(0).value;
+
+      if (removedAcl?.source_id) {
+        this.removedAclSourceIds.push(removedAcl.source_id);
+      }
+      const newGroup = this.createAcls();
+      this.acls.setControl(0, newGroup);
+    }
+
+    this.cdRef.detectChanges();
+  }
+
+  getAclLabel(field: AclFieldType, type?: string): string {
+    if (!type) {
+      return field === AclFieldType.Source ? AclLabel.source : AclLabel.destination;
+    }
+    return (
+      this.aclTypeLabel[type]?.[field] ||
+      (field === AclFieldType.Source ? AclLabel.source : AclLabel.destination)
+    );
+  }
+
+  getAclHelperText(type: string, field: AclFieldType): string {
+    return this.aclHelperText[type]?.[field] || '';
+  }
+
+  storageClassTypeText() {
+    this.storageClassForm?.get('storageClassType')?.valueChanges.subscribe((value) => {
+      if (value === TIER_TYPE.LOCAL) {
+        this.helpTextLabels.storageClassText = LOCAL_STORAGE_CLASS_TEXT;
+      } else if (value === TIER_TYPE.CLOUD_TIER) {
+        this.helpTextLabels.storageClassText = CLOUDS3_STORAGE_CLASS_TEXT;
+      } else if (value === TIER_TYPE.GLACIER) {
+        this.helpTextLabels.storageClassText = GLACIER_STORAGE_CLASS_TEXT;
+      }
+    });
+  }
+
+  updateTierTypeHelpText() {
+    this.storageClassForm?.get('glacier_restore_tier_type')?.valueChanges.subscribe((value) => {
+      if (value === STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS) {
+        this.helpTextLabels.tiertypeText = STANDARD_TIER_TYPE_TEXT;
+      } else {
+        this.helpTextLabels.tiertypeText = EXPEDITED_TIER_TYPE_TEXT;
+      }
     });
   }
 
@@ -426,6 +554,23 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     const multipart_sync_threshold = this.formatter.toBytes(
       this.storageClassForm.get('multipart_sync_threshold').value
     );
+
+    const removeAclList: ACLVal[] = rawFormValue.acls || [];
+    const tier_config_rm: TierConfigRm = {};
+    this.removedAclSourceIds.forEach((sourceId: string, index: number) => {
+      tier_config_rm[`acls[${index}].source_id`] = sourceId;
+    });
+    if (this.aclList.length > rawFormValue.acls.length) {
+      this.aclList.forEach((acl: ACL, index: number) => {
+        const sourceId = acl?.val?.source_id;
+        const ifExist = removeAclList.find((acl: ACLVal) => acl?.source_id === sourceId);
+
+        if (!ifExist) {
+          tier_config_rm[`acls[${index}].source_id`] = sourceId;
+        }
+      });
+    }
+
     return this.buildPlacementTargets(
       storageClassType,
       zoneGroup,
@@ -434,7 +579,8 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       retain_head_object,
       rawFormValue,
       multipart_sync_threshold,
-      multipart_min_part_size
+      multipart_min_part_size,
+      tier_config_rm
     );
   }
 
@@ -446,11 +592,13 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     retain_head_object: boolean,
     rawFormValue: any,
     multipart_sync_threshold: number,
-    multipart_min_part_size: number
+    multipart_min_part_size: number,
+    tier_config_rm: TierConfigRm
   ): RequestModel {
     const baseTarget = {
       placement_id: placementId,
-      storage_class: storageClass
+      storage_class: storageClass,
+      tier_config_rm: tier_config_rm
     };
 
     if (storageClassType === TIER_TYPE.LOCAL) {
@@ -460,6 +608,19 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       };
     }
 
+    const aclConfig: { [key: string]: string } = {};
+
+    rawFormValue.acls.forEach((acl: ACLVal, index: number) => {
+      const sourceId = acl?.source_id?.trim();
+      if (!sourceId) return;
+
+      const destId = acl?.dest_id?.trim() || '';
+      const type = acl?.type?.trim() || AclTypeConst.ID;
+
+      aclConfig[`acls[${index}].source_id`] = sourceId;
+      aclConfig[`acls[${index}].dest_id`] = destId;
+      aclConfig[`acls[${index}].type`] = type as AclType;
+    });
     const tierConfig = {
       endpoint: rawFormValue.target_endpoint,
       access_key: rawFormValue.access_key,
@@ -468,12 +629,13 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       retain_head_object,
       allow_read_through: rawFormValue.allow_read_through,
       region: rawFormValue.region,
-      multipart_sync_threshold: multipart_sync_threshold,
-      multipart_min_part_size: multipart_min_part_size,
+      multipart_sync_threshold,
+      multipart_min_part_size,
       restore_storage_class: rawFormValue.restore_storage_class,
       ...(rawFormValue.allow_read_through
         ? { read_through_restore_days: rawFormValue.read_through_restore_days }
-        : {})
+        : {}),
+      ...aclConfig
     };
 
     if (storageClassType === TIER_TYPE.CLOUD_TIER) {
@@ -483,6 +645,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
           {
             ...baseTarget,
             tier_type: TIER_TYPE.CLOUD_TIER,
+            tier_config_rm: tier_config_rm,
             tier_config: {
               ...tierConfig
             }
@@ -498,6 +661,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
           {
             ...baseTarget,
             tier_type: TIER_TYPE.GLACIER,
+            tier_config_rm: tier_config_rm,
             tier_config: {
               ...tierConfig,
               glacier_restore_days: rawFormValue.glacier_restore_days,
@@ -508,6 +672,8 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
         ]
       };
     }
+
+    this.removedAclSourceIds = [];
     return {
       zone_group: zoneGroup,
       placement_targets: [baseTarget]
index ba5bd46f9d040f6b92551e2f588e09c79a1b514d..c6896d62fa04cee6fcadfa7403acbc7193413216 100644 (file)
@@ -45,6 +45,7 @@ export class BucketTieringUtils {
       allow_read_through: val.allow_read_through,
       restore_storage_class: val.restore_storage_class,
       read_through_restore_days: val.read_through_restore_days,
+      acls: val.s3.acl_mappings,
       ...val.s3
     };
 
index 892e6458705bcf1c9bf962974645b13127484497..06d7b7c2133c0d4981b37e9fceb7b3dc5449a410 100644 (file)
@@ -41,6 +41,7 @@ describe('RgwStorageClassService', () => {
           placement_id: 'default-placement',
           storage_class: 'test1',
           tier_type: 'cloud-s3',
+          tier_config_rm: { 'acls.source_id': 'test1' },
           tier_config: {
             endpoint: 'http://198.162.100.100:80',
             access_key: 'test56',
index d47b969f8ddd36baa2b14556c9616893155bd067..a90fc6e96138ff69eb9a9969e9b5f6bd098fcf5a 100755 (executable)
@@ -2224,6 +2224,13 @@ class RgwMultisite:
                         '--tier-type', tier_type, '--tier-config', tier_config_str
                     ]
 
+            tier_config_rm = placement_target.get('tier_config_rm', {})
+            if tier_config_rm:
+                tier_config_rm_str = ','.join(
+                    f"{key}={value}" for key, value in tier_config_rm.items()
+                )
+                cmd_add_placement_options += ['--tier-config-rm', tier_config_rm_str]
+
             if placement_target.get('tags') and storage_class_name != STANDARD_STORAGE_CLASS:
                 cmd_add_placement_options += ['--tags', placement_target['tags']]