Skip to content

Commit b54efd4

Browse files
committed
mgr/dashboard: add RGW lifecycle management
Fixes: https://tracker.ceph.com/issues/50327 Signed-off-by: Pedro Gonzalez Gomez <[email protected]>
1 parent 6fc2885 commit b54efd4

20 files changed

+497
-62
lines changed

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,18 @@ def _set_tags(self, bucket_name, tags, daemon_name, owner):
391391
rgw_client = RgwClient.instance(owner, daemon_name)
392392
return rgw_client.set_tags(bucket_name, tags)
393393

394+
def _get_lifecycle(self, bucket_name: str, daemon_name, owner):
395+
rgw_client = RgwClient.instance(owner, daemon_name)
396+
return rgw_client.get_lifecycle(bucket_name)
397+
398+
def _set_lifecycle(self, bucket_name: str, lifecycle: str, daemon_name, owner):
399+
rgw_client = RgwClient.instance(owner, daemon_name)
400+
return rgw_client.set_lifecycle(bucket_name, lifecycle)
401+
402+
def _delete_lifecycle(self, bucket_name: str, daemon_name, owner):
403+
rgw_client = RgwClient.instance(owner, daemon_name)
404+
return rgw_client.delete_lifecycle(bucket_name)
405+
394406
def _get_acl(self, bucket_name, daemon_name, owner):
395407
rgw_client = RgwClient.instance(owner, daemon_name)
396408
return str(rgw_client.get_acl(bucket_name))
@@ -473,6 +485,7 @@ def get(self, bucket, daemon_name=None):
473485
result['bucket_policy'] = self._get_policy(bucket_name, daemon_name, result['owner'])
474486
result['acl'] = self._get_acl(bucket_name, daemon_name, result['owner'])
475487
result['replication'] = self._get_replication(bucket_name, result['owner'], daemon_name)
488+
result['lifecycle'] = self._get_lifecycle(bucket_name, daemon_name, result['owner'])
476489

477490
# Append the locking configuration.
478491
locking = self._get_locking(result['owner'], daemon_name, bucket_name)
@@ -525,7 +538,8 @@ def set(self, bucket, bucket_id, uid, versioning_state=None,
525538
mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None,
526539
lock_mode=None, lock_retention_period_days=None,
527540
lock_retention_period_years=None, tags=None, bucket_policy=None,
528-
canned_acl=None, replication=None, daemon_name=None):
541+
canned_acl=None, replication=None, lifecycle=None, daemon_name=None):
542+
# pylint: disable=R0912
529543
encryption_state = str_to_bool(encryption_state)
530544
if replication is not None:
531545
replication = str_to_bool(replication)
@@ -573,8 +587,12 @@ def set(self, bucket, bucket_id, uid, versioning_state=None,
573587
self._set_policy(bucket_name, bucket_policy, daemon_name, uid)
574588
if canned_acl:
575589
self._set_acl(bucket_name, canned_acl, uid, daemon_name)
576-
if replication is not None:
590+
if replication:
577591
self._set_replication(bucket_name, replication, uid, daemon_name)
592+
if lifecycle and not lifecycle == '{}':
593+
self._set_lifecycle(bucket_name, lifecycle, daemon_name, uid)
594+
else:
595+
self._delete_lifecycle(bucket_name, daemon_name, uid)
578596
return self._append_bid(result)
579597

580598
def delete(self, bucket, purge_objects='true', daemon_name=None):

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

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -127,49 +127,76 @@
127127
<a ngbNavLink
128128
i18n>Policies</a>
129129
<ng-template ngbNavContent>
130-
131-
<table class="table table-striped table-bordered">
132-
<tbody>
133-
<tr>
134-
<td i18n
135-
class="bold w-25">Bucket policy</td>
136-
<td><pre>{{ selection.bucket_policy | json}}</pre></td>
137-
</tr>
138-
<tr>
139-
<td i18n
140-
class="bold w-25">Replication policy</td>
141-
<td><pre>{{ selection.replication | json}}</pre></td>
142-
</tr>
143-
<tr>
144-
<td i18n
145-
class="bold w-25">ACL</td>
146-
<td>
147-
<table class="table">
148-
<thead>
149-
<tr i18n>
150-
<th>Grantee</th>
151-
<th>Permissions</th>
152-
</tr>
153-
</thead>
154-
<tbody>
155-
<tr i18n>
156-
<td>Bucket Owner</td>
157-
<td>{{ aclPermissions.Owner || '-'}}</td>
158-
</tr>
159-
<tr i18n>
160-
<td>Everyone</td>
161-
<td>{{ aclPermissions.AllUsers || '-'}}</td>
162-
</tr>
163-
<tr i18n>
164-
<td>Authenticated users group</td>
165-
<td>{{ aclPermissions.AuthenticatedUsers || '-'}}</td>
166-
</tr>
167-
</tbody>
168-
</table>
169-
</td>
170-
</tr>
171-
</tbody>
172-
</table>
130+
<div class="table-scroller">
131+
<table class="table table-striped table-bordered">
132+
<tbody>
133+
<tr>
134+
<td i18n
135+
class="bold w-25">Bucket policy</td>
136+
<td><pre>{{ selection.bucket_policy | json}}</pre></td>
137+
</tr>
138+
<tr>
139+
<div>
140+
<td i18n
141+
class="bold w-25">Lifecycle
142+
<div *ngIf="(selection.lifecycle | json) !== '{}'"
143+
class="input-group">
144+
<button type="button"
145+
class="btn btn-light"
146+
[ngClass]="{'active': lifecycleFormat === 'json'}"
147+
(click)="lifecycleFormat = 'json'">
148+
JSON
149+
</button>
150+
<button type="button"
151+
class="btn btn-light"
152+
[ngClass]="{'active': lifecycleFormat === 'xml'}"
153+
(click)="lifecycleFormat = 'xml'">
154+
XML
155+
</button>
156+
</div>
157+
</td>
158+
</div>
159+
<td>
160+
<pre *ngIf="lifecycleFormat === 'json'">{{selection.lifecycle | json}}</pre>
161+
<pre *ngIf="lifecycleFormat === 'xml'">{{ (selection.lifecycle | xml) || '-'}}</pre>
162+
</td>
163+
</tr>
164+
<tr>
165+
<td i18n
166+
class="bold w-25">Replication policy</td>
167+
<td><pre>{{ selection.replication | json}}</pre></td>
168+
</tr>
169+
<tr>
170+
<td i18n
171+
class="bold w-25">ACL</td>
172+
<td>
173+
<table class="table">
174+
<thead>
175+
<tr i18n>
176+
<th>Grantee</th>
177+
<th>Permissions</th>
178+
</tr>
179+
</thead>
180+
<tbody>
181+
<tr i18n>
182+
<td>Bucket Owner</td>
183+
<td>{{ aclPermissions.Owner || '-'}}</td>
184+
</tr>
185+
<tr i18n>
186+
<td>Everyone</td>
187+
<td>{{ aclPermissions.AllUsers || '-'}}</td>
188+
</tr>
189+
<tr i18n>
190+
<td>Authenticated users group</td>
191+
<td>{{ aclPermissions.AuthenticatedUsers || '-'}}</td>
192+
</tr>
193+
</tbody>
194+
</table>
195+
</td>
196+
</tr>
197+
</tbody>
198+
</table>
199+
</div>
173200
</ng-template>
174201
</ng-container>
175202
</nav>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ table {
55
table td {
66
word-wrap: break-word;
77
}
8+
9+
.table-scroller {
10+
height: 100%;
11+
max-height: 50vh;
12+
overflow: auto;
13+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class RgwBucketDetailsComponent implements OnChanges {
1313
@Input()
1414
selection: any;
1515

16+
lifecycleFormat: 'json' | 'xml' = 'json';
1617
aclPermissions: Record<string, string[]> = {};
1718
replicationStatus = $localize`Disabled`;
1819

@@ -23,6 +24,9 @@ export class RgwBucketDetailsComponent implements OnChanges {
2324
this.rgwBucketService.get(this.selection.bid).subscribe((bucket: object) => {
2425
bucket['lock_retention_period_days'] = this.rgwBucketService.getLockDays(bucket);
2526
this.selection = bucket;
27+
if (this.lifecycleFormat === 'json' && !this.selection.lifecycle) {
28+
this.selection.lifecycle = {};
29+
}
2630
this.aclPermissions = this.parseXmlAcl(this.selection.acl, this.selection.owner);
2731
if (this.selection.replication?.['Rule']?.['Status']) {
2832
this.replicationStatus = this.selection.replication?.['Rule']?.['Status'];

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -467,15 +467,15 @@
467467
class="form-control resize-vertical"
468468
id="bucket_policy"
469469
formControlName="bucket_policy"
470-
(change)="bucketPolicyOnChange()">
470+
(change)="textAreaOnChange('bucketPolicyTextArea')">
471471
</textarea>
472472
<span class="invalid-feedback"
473473
*ngIf="bucketForm.showError('bucket_policy', frm, 'invalidJson')"
474-
i18n>Invalid json text</span>
474+
i18n>Invalid json text.</span>
475475
<button type="button"
476476
id="clear-bucket-policy"
477477
class="btn btn-light my-3"
478-
(click)="clearBucketPolicy()"
478+
(click)="clearTextArea('bucket_policy', '{}')"
479479
i18n>
480480
<i [ngClass]="[icons.destroy]"></i>
481481
Clear
@@ -503,6 +503,50 @@
503503
</div>
504504
</div>
505505

506+
<!-- Lifecycle -->
507+
<div *ngIf="editing"
508+
class="form-group row">
509+
<label i18n
510+
class="cd-col-form-label"
511+
for="id">Lifecycle
512+
<cd-helper>JSON or XML formatted document</cd-helper>
513+
</label>
514+
<div class="cd-col-form-input">
515+
<textarea #lifecycleTextArea
516+
class="form-control resize-vertical"
517+
id="lifecycle"
518+
formControlName="lifecycle"
519+
(change)="textAreaOnChange('lifecycleTextArea')">
520+
</textarea>
521+
<span class="invalid-feedback"
522+
*ngIf="bucketForm.showError('lifecycle', frm, 'invalidJson')"
523+
i18n>Invalid json text.</span>
524+
<span class="invalid-feedback"
525+
*ngIf="bucketForm.showError('lifecycle', frm, 'invalidXml')"
526+
i18n>Invalid xml text.</span>
527+
<button type="button"
528+
id="clear-lifecycle"
529+
class="btn btn-light my-3"
530+
(click)="clearTextArea('lifecycle', '{}')"
531+
i18n>
532+
<i [ngClass]="[icons.destroy]"></i>
533+
Clear
534+
</button>
535+
<div class="btn-group float-end"
536+
role="group"
537+
aria-label="bucket-policy-helpers">
538+
<button type="button"
539+
id="lifecycle-examples-button"
540+
class="btn btn-light my-3"
541+
(click)="openUrl('https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-lifecycle.html#examples')"
542+
i18n>
543+
<i [ngClass]="[icons.externalUrl]"></i>
544+
Policy examples
545+
</button>
546+
</div>
547+
</div>
548+
</div>
549+
506550
<div class="form-group row">
507551

508552
<!-- ACL -->

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { TextAreaJsonFormatterService } from '~/app/shared/services/text-area-js
3939
import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
4040
import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
4141
import { map, switchMap } from 'rxjs/operators';
42+
import { TextAreaXmlFormatterService } from '~/app/shared/services/text-area-xml-formatter.service';
4243

4344
@Component({
4445
selector: 'cd-rgw-bucket-form',
@@ -49,6 +50,8 @@ import { map, switchMap } from 'rxjs/operators';
4950
export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewChecked {
5051
@ViewChild('bucketPolicyTextArea')
5152
public bucketPolicyTextArea: ElementRef<any>;
53+
@ViewChild('lifecycleTextArea')
54+
public lifecycleTextArea: ElementRef<any>;
5255

5356
bucketForm: CdFormGroup;
5457
editing = false;
@@ -96,6 +99,7 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
9699
private notificationService: NotificationService,
97100
private rgwEncryptionModal: RgwBucketEncryptionModel,
98101
private textAreaJsonFormatterService: TextAreaJsonFormatterService,
102+
private textAreaXmlFormatterService: TextAreaXmlFormatterService,
99103
public actionLabels: ActionLabelsI18n,
100104
private readonly changeDetectorRef: ChangeDetectorRef,
101105
private rgwMultisiteService: RgwMultisiteService,
@@ -110,7 +114,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
110114

111115
ngAfterViewChecked(): void {
112116
this.changeDetectorRef.detectChanges();
113-
this.bucketPolicyOnChange();
117+
this.textAreaOnChange(this.bucketPolicyTextArea);
118+
this.textAreaOnChange(this.lifecycleTextArea);
114119
}
115120

116121
createForm() {
@@ -160,6 +165,7 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
160165
lock_mode: ['COMPLIANCE'],
161166
lock_retention_period_days: [10, [CdValidators.number(false), lockDaysValidator]],
162167
bucket_policy: ['{}', CdValidators.json()],
168+
lifecycle: ['{}', CdValidators.jsonOrXml()],
163169
grantee: [Grantee.Owner, [Validators.required]],
164170
aclPermission: [[aclPermission.FullControl], [Validators.required]],
165171
replication: [false]
@@ -257,6 +263,7 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
257263
bidResp['acl'],
258264
bidResp['owner']
259265
);
266+
value['lifecycle'] = JSON.stringify(bidResp['lifecycle'] || {});
260267
}
261268
this.bucketForm.setValue(value);
262269
if (this.editing) {
@@ -335,7 +342,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
335342
xmlStrTags,
336343
bucketPolicy,
337344
cannedAcl,
338-
values['replication']
345+
values['replication'],
346+
values['lifecycle']
339347
)
340348
.subscribe(
341349
() => {
@@ -433,18 +441,20 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
433441
});
434442
}
435443

436-
bucketPolicyOnChange() {
437-
if (this.bucketPolicyTextArea) {
438-
this.textAreaJsonFormatterService.format(this.bucketPolicyTextArea);
444+
textAreaOnChange(textArea: ElementRef<any>) {
445+
if (textArea?.nativeElement?.value?.startsWith?.('<')) {
446+
this.textAreaXmlFormatterService.format(textArea);
447+
} else {
448+
this.textAreaJsonFormatterService.format(textArea);
439449
}
440450
}
441451

442452
openUrl(url: string) {
443453
window.open(url, '_blank');
444454
}
445455

446-
clearBucketPolicy() {
447-
this.bucketForm.get('bucket_policy').setValue('{}');
456+
clearTextArea(field: string, defaultValue: string = '') {
457+
this.bucketForm.get(field).setValue(defaultValue);
448458
this.bucketForm.markAsDirty();
449459
this.bucketForm.updateValueAndValidity();
450460
}

src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,12 @@ describe('RgwBucketService', () => {
9090
null,
9191
null,
9292
'private',
93-
'true'
93+
'true',
94+
null
9495
)
9596
.subscribe();
9697
const req = httpTesting.expectOne(
97-
`api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=10&tags=null&bucket_policy=null&canned_acl=private&replication=true`
98+
`api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=10&tags=null&bucket_policy=null&canned_acl=private&replication=true&lifecycle=null`
9899
);
99100
expect(req.request.method).toBe('PUT');
100101
});

src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ export class RgwBucketService extends ApiClient {
109109
tags: string,
110110
bucketPolicy: string,
111111
cannedAcl: string,
112-
replication: string
112+
replication: string,
113+
lifecycle: string
113114
) {
114115
return this.rgwDaemonService.request((params: HttpParams) => {
115116
params = params.appendAll({
@@ -127,7 +128,8 @@ export class RgwBucketService extends ApiClient {
127128
tags: tags,
128129
bucket_policy: bucketPolicy,
129130
canned_acl: cannedAcl,
130-
replication: replication
131+
replication: replication,
132+
lifecycle: lifecycle
131133
});
132134
return this.http.put(`${this.url}/${bucket}`, null, { params: params });
133135
});

0 commit comments

Comments
 (0)