Skip to content

Commit 213b2d8

Browse files
authored
Merge pull request ceph#61690 from rhcs-dashboard/bucket-tiering
mgr/dashboard: bucket lifecycle policy management Reviewed-by: Aashish Sharma <[email protected]>
2 parents e0e3dc9 + 109c75e commit 213b2d8

23 files changed

+1074
-47
lines changed

src/pybind/mgr/dashboard/controllers/rgw.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,10 @@ def _set_tags(self, bucket_name, tags, daemon_name, owner):
465465
rgw_client = RgwClient.instance(owner, daemon_name)
466466
return rgw_client.set_tags(bucket_name, tags)
467467

468+
def _get_lifecycle_progress(self):
469+
rgw_client = RgwClient.admin_instance()
470+
return rgw_client.get_lifecycle_progress()
471+
468472
def _get_lifecycle(self, bucket_name: str, daemon_name, owner):
469473
rgw_client = RgwClient.instance(owner, daemon_name)
470474
return rgw_client.get_lifecycle(bucket_name)
@@ -561,7 +565,7 @@ def get(self, bucket, daemon_name=None):
561565
result['acl'] = self._get_acl(bucket_name, daemon_name, owner)
562566
result['replication'] = self._get_replication(bucket_name, owner, daemon_name)
563567
result['lifecycle'] = self._get_lifecycle(bucket_name, daemon_name, owner)
564-
568+
result['lifecycle_progress'] = self._get_lifecycle_progress()
565569
# Append the locking configuration.
566570
locking = self._get_locking(owner, daemon_name, bucket_name)
567571
result.update(locking)
@@ -706,6 +710,18 @@ def delete_encryption(self, bucket_name, daemon_name=None, owner=None):
706710
def get_encryption_config(self, daemon_name=None, owner=None):
707711
return CephService.get_encryption_config(daemon_name)
708712

713+
@RESTController.Collection(method='PUT', path='/lifecycle')
714+
@allow_empty_body
715+
def set_lifecycle_policy(self, bucket_name: str = '', lifecycle: str = '', daemon_name=None,
716+
owner=None):
717+
if lifecycle == '{}':
718+
return self._delete_lifecycle(bucket_name, daemon_name, owner)
719+
return self._set_lifecycle(bucket_name, lifecycle, daemon_name, owner)
720+
721+
@RESTController.Collection(method='GET', path='/lifecycle')
722+
def get_lifecycle_policy(self, bucket_name: str = '', daemon_name=None, owner=None):
723+
return self._get_lifecycle(bucket_name, daemon_name, owner)
724+
709725

710726
@UIRouter('/rgw/bucket', Scope.RGW)
711727
class RgwBucketUi(RgwBucket):
Original file line numberDiff line numberDiff line change
@@ -1,3 +0,0 @@
1-
.item-action-btn {
2-
margin-top: 2rem;
3-
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,19 @@
168168
</cds-code-snippet>
169169
</td>
170170
</tr>
171+
<tr *ngIf="selection.lifecycle_progress?.length > 0">
172+
<td i18n
173+
class="bold w-25">Lifecycle Progress</td>
174+
<td>
175+
<cds-tooltip [description]="lifecycleProgressMap.get(lifecycleProgress)?.description"
176+
[align]="'top'">
177+
<cds-tag size="md"
178+
[type]="lifecycleProgressMap.get(lifecycleProgress)?.color">
179+
{{ lifecycleProgress }}
180+
</cds-tag>
181+
</cds-tooltip>
182+
</td>
183+
</tr>
171184
<tr>
172185
<td i18n
173186
class="bold w-25">Replication policy</td>
@@ -206,6 +219,14 @@
206219
</div>
207220
</ng-template>
208221
</ng-container>
222+
223+
<ng-container ngbNavItem="tiering">
224+
<a ngbNavLink
225+
i18n>Tiering</a>
226+
<ng-template ngbNavContent>
227+
<cd-rgw-bucket-lifecycle-list [bucket]="selection"></cd-rgw-bucket-lifecycle-list>
228+
</ng-template>
229+
</ng-container>
209230
</nav>
210231

211232
<div [ngbNavOutlet]="nav"></div>

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import * as xml2js from 'xml2js';
1212
export class RgwBucketDetailsComponent implements OnChanges {
1313
@Input()
1414
selection: any;
15-
15+
lifecycleProgress: string;
16+
lifecycleProgressMap = new Map<string, { description: string; color: string }>([
17+
['UNINITIAL', { description: $localize`The process has not run yet`, color: 'cool-gray' }],
18+
['PROCESSING', { description: $localize`The process is currently running`, color: 'cyan' }],
19+
['COMPLETE', { description: $localize`The process has completed`, color: 'green' }]
20+
]);
1621
lifecycleFormat: 'json' | 'xml' = 'json';
1722
aclPermissions: Record<string, string[]> = {};
1823
replicationStatus = $localize`Disabled`;
@@ -31,6 +36,15 @@ export class RgwBucketDetailsComponent implements OnChanges {
3136
if (this.selection.replication?.['Rule']?.['Status']) {
3237
this.replicationStatus = this.selection.replication?.['Rule']?.['Status'];
3338
}
39+
if (this.selection.lifecycle_progress?.length > 0) {
40+
this.selection.lifecycle_progress.forEach(
41+
(progress: { bucket: string; status: string; started: string }) => {
42+
if (progress.bucket.includes(this.selection.bucket)) {
43+
this.lifecycleProgress = progress.status;
44+
}
45+
}
46+
);
47+
}
3448
});
3549
}
3650
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<legend i18n>
2+
Tiering Configuration
3+
<cd-help-text>
4+
Configure a bucket tiering rule to automatically transition objects between storage classes after a specified number of days. Define the scope of the rule by applying it globally or to objects with specific prefixes and tags.
5+
</cd-help-text>
6+
</legend>
7+
<cd-table #table
8+
[data]="filteredLifecycleRules$ | async"
9+
[columns]="columns"
10+
columnMode="flex"
11+
selectionType="multiClick"
12+
(updateSelection)="updateSelection($event)"
13+
identifier="ID"
14+
(fetchData)="loadLifecyclePolicies($event)">
15+
<cd-table-actions class="table-actions"
16+
[permission]="permission"
17+
[selection]="selection"
18+
[tableActions]="tableActions">
19+
</cd-table-actions>
20+
</cd-table>

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-lifecycle-list/rgw-bucket-lifecycle-list.component.scss

Whitespace-only changes.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { RgwBucketLifecycleListComponent } from './rgw-bucket-lifecycle-list.component';
3+
import { configureTestBed } from '~/testing/unit-test-helper';
4+
import { of } from 'rxjs';
5+
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
6+
import { ReactiveFormsModule } from '@angular/forms';
7+
import { ToastrModule } from 'ngx-toastr';
8+
import { ComponentsModule } from '~/app/shared/components/components.module';
9+
import {
10+
InputModule,
11+
ModalModule,
12+
ModalService,
13+
NumberModule,
14+
RadioModule,
15+
SelectModule
16+
} from 'carbon-components-angular';
17+
import { CdLabelComponent } from '~/app/shared/components/cd-label/cd-label.component';
18+
19+
class MockRgwBucketService {
20+
setLifecycle = jest.fn().mockReturnValue(of(null));
21+
getLifecycle = jest.fn().mockReturnValue(of(null));
22+
}
23+
24+
describe('RgwBucketLifecycleListComponent', () => {
25+
let component: RgwBucketLifecycleListComponent;
26+
let fixture: ComponentFixture<RgwBucketLifecycleListComponent>;
27+
28+
configureTestBed({
29+
declarations: [RgwBucketLifecycleListComponent, CdLabelComponent],
30+
imports: [
31+
ReactiveFormsModule,
32+
RadioModule,
33+
SelectModule,
34+
NumberModule,
35+
InputModule,
36+
ToastrModule.forRoot(),
37+
ComponentsModule,
38+
ModalModule
39+
],
40+
providers: [
41+
ModalService,
42+
{ provide: 'bucket', useValue: { bucket: 'bucket1', owner: 'dashboard' } },
43+
{ provide: RgwBucketService, useClass: MockRgwBucketService }
44+
]
45+
});
46+
47+
beforeEach(() => {
48+
fixture = TestBed.createComponent(RgwBucketLifecycleListComponent);
49+
component = fixture.componentInstance;
50+
fixture.detectChanges();
51+
});
52+
53+
it('should create', () => {
54+
expect(component).toBeTruthy();
55+
});
56+
});
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { Component, Input, OnInit, ViewChild } from '@angular/core';
2+
import { Bucket } from '../models/rgw-bucket';
3+
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
4+
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
5+
import { Icons } from '~/app/shared/enum/icons.enum';
6+
import { CdTableAction } from '~/app/shared/models/cd-table-action';
7+
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
8+
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
9+
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
10+
import { Permission } from '~/app/shared/models/permissions';
11+
import { TableComponent } from '~/app/shared/datatable/table/table.component';
12+
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
13+
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
14+
import { RgwBucketTieringFormComponent } from '../rgw-bucket-tiering-form/rgw-bucket-tiering-form.component';
15+
import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
16+
import { Observable, of } from 'rxjs';
17+
import { catchError, map, tap } from 'rxjs/operators';
18+
import { NotificationService } from '~/app/shared/services/notification.service';
19+
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
20+
21+
@Component({
22+
selector: 'cd-rgw-bucket-lifecycle-list',
23+
templateUrl: './rgw-bucket-lifecycle-list.component.html',
24+
styleUrls: ['./rgw-bucket-lifecycle-list.component.scss']
25+
})
26+
export class RgwBucketLifecycleListComponent implements OnInit {
27+
@Input() bucket: Bucket;
28+
@ViewChild(TableComponent, { static: true })
29+
table: TableComponent;
30+
permission: Permission;
31+
tableActions: CdTableAction[];
32+
columns: CdTableColumn[] = [];
33+
selection: CdTableSelection = new CdTableSelection();
34+
filteredLifecycleRules$: Observable<any[]>;
35+
lifecycleRuleList: any = [];
36+
modalRef: any;
37+
38+
constructor(
39+
private rgwBucketService: RgwBucketService,
40+
private authStorageService: AuthStorageService,
41+
public actionLabels: ActionLabelsI18n,
42+
private modalService: ModalCdsService,
43+
private notificationService: NotificationService
44+
) {}
45+
46+
ngOnInit() {
47+
this.permission = this.authStorageService.getPermissions().rgw;
48+
this.columns = [
49+
{
50+
name: $localize`Name`,
51+
prop: 'ID',
52+
flexGrow: 2
53+
},
54+
{
55+
name: $localize`Days`,
56+
prop: 'Transition.Days',
57+
flexGrow: 1
58+
},
59+
{
60+
name: $localize`Storage class`,
61+
prop: 'Transition.StorageClass',
62+
flexGrow: 1
63+
},
64+
{
65+
name: $localize`Status`,
66+
prop: 'Status',
67+
flexGrow: 1
68+
}
69+
];
70+
const createAction: CdTableAction = {
71+
permission: 'create',
72+
icon: Icons.add,
73+
click: () => this.openTieringModal(this.actionLabels.CREATE),
74+
name: this.actionLabels.CREATE
75+
};
76+
const editAction: CdTableAction = {
77+
permission: 'update',
78+
icon: Icons.edit,
79+
disable: () => this.selection.hasMultiSelection,
80+
click: () => this.openTieringModal(this.actionLabels.EDIT),
81+
name: this.actionLabels.EDIT
82+
};
83+
const deleteAction: CdTableAction = {
84+
permission: 'delete',
85+
icon: Icons.destroy,
86+
click: () => this.deleteAction(),
87+
disable: () => !this.selection.hasSelection,
88+
name: this.actionLabels.DELETE,
89+
canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
90+
};
91+
this.tableActions = [createAction, editAction, deleteAction];
92+
}
93+
94+
loadLifecyclePolicies(context: CdTableFetchDataContext) {
95+
const allLifecycleRules$ = this.rgwBucketService
96+
.getLifecycle(this.bucket.bucket, this.bucket.owner)
97+
.pipe(
98+
tap((lifecycle) => {
99+
this.lifecycleRuleList = lifecycle;
100+
}),
101+
catchError(() => {
102+
context.error();
103+
return of(null);
104+
})
105+
);
106+
107+
this.filteredLifecycleRules$ = allLifecycleRules$.pipe(
108+
map(
109+
(lifecycle: any) =>
110+
lifecycle?.LifecycleConfiguration?.Rules?.filter((rule: object) =>
111+
rule.hasOwnProperty('Transition')
112+
) || []
113+
)
114+
);
115+
}
116+
117+
openTieringModal(type: string) {
118+
this.modalService.show(RgwBucketTieringFormComponent, {
119+
bucket: this.bucket,
120+
selectedLifecycle: this.selection.first(),
121+
editing: type === this.actionLabels.EDIT ? true : false
122+
});
123+
}
124+
125+
updateSelection(selection: CdTableSelection) {
126+
this.selection = selection;
127+
}
128+
129+
deleteAction() {
130+
const ruleNames = this.selection.selected.map((rule) => rule.ID);
131+
const filteredRules = this.lifecycleRuleList.LifecycleConfiguration.Rules.filter(
132+
(rule: any) => !ruleNames.includes(rule.ID)
133+
);
134+
const rules = filteredRules.length > 0 ? { Rules: filteredRules } : {};
135+
this.modalRef = this.modalService.show(DeleteConfirmationModalComponent, {
136+
itemDescription: $localize`Rule`,
137+
itemNames: ruleNames,
138+
actionDescription: $localize`remove`,
139+
submitAction: () => this.submitLifecycleConfig(rules)
140+
});
141+
}
142+
143+
submitLifecycleConfig(rules: any) {
144+
this.rgwBucketService
145+
.setLifecycle(this.bucket.bucket, JSON.stringify(rules), this.bucket.owner)
146+
.subscribe({
147+
next: () => {
148+
this.notificationService.show(
149+
NotificationType.success,
150+
$localize`Lifecycle rule deleted successfully`
151+
);
152+
},
153+
error: () => {
154+
this.modalRef.componentInstance.stopLoadingSpinner();
155+
},
156+
complete: () => {
157+
this.modalService.dismissAll();
158+
}
159+
});
160+
}
161+
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('RgwBucketListComponent', () => {
5555

5656
expect(tableActions).toEqual({
5757
'create,update,delete': {
58-
actions: ['Create', 'Edit', 'Delete'],
58+
actions: ['Create', 'Edit', 'Delete', 'Tiering'],
5959
primary: {
6060
multiple: 'Create',
6161
executing: 'Create',
@@ -64,7 +64,7 @@ describe('RgwBucketListComponent', () => {
6464
}
6565
},
6666
'create,update': {
67-
actions: ['Create', 'Edit'],
67+
actions: ['Create', 'Edit', 'Tiering'],
6868
primary: {
6969
multiple: 'Create',
7070
executing: 'Create',
@@ -91,7 +91,7 @@ describe('RgwBucketListComponent', () => {
9191
}
9292
},
9393
'update,delete': {
94-
actions: ['Edit', 'Delete'],
94+
actions: ['Edit', 'Delete', 'Tiering'],
9595
primary: {
9696
multiple: '',
9797
executing: '',
@@ -100,12 +100,12 @@ describe('RgwBucketListComponent', () => {
100100
}
101101
},
102102
update: {
103-
actions: ['Edit'],
103+
actions: ['Edit', 'Tiering'],
104104
primary: {
105-
multiple: 'Edit',
106-
executing: 'Edit',
107-
single: 'Edit',
108-
no: 'Edit'
105+
multiple: '',
106+
executing: '',
107+
single: '',
108+
no: ''
109109
}
110110
},
111111
delete: {

0 commit comments

Comments
 (0)