rgw_client = RgwClient.admin_instance()
return rgw_client.get_bucket_policy(bucket)
+ def _set_tags(self, bucket_name, tags, daemon_name, owner):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ return rgw_client.set_tags(bucket_name, tags)
+
@staticmethod
def strip_tenant_from_bucket_name(bucket_name):
# type (str) -> str
lock_enabled='false', lock_mode=None,
lock_retention_period_days=None,
lock_retention_period_years=None, encryption_state='false',
- encryption_type=None, key_id=None, daemon_name=None):
+ encryption_type=None, key_id=None, tags=None, daemon_name=None):
lock_enabled = str_to_bool(lock_enabled)
encryption_state = str_to_bool(encryption_state)
try:
if encryption_state:
self._set_encryption(bucket, encryption_type, key_id, daemon_name, uid)
+ if tags:
+ self._set_tags(bucket, tags, daemon_name, uid)
+
return result
except RequestException as e: # pragma: no cover - handling is too obvious
raise DashboardException(e, http_status_code=500, component='rgw')
encryption_state='false', encryption_type=None, key_id=None,
mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None,
lock_mode=None, lock_retention_period_days=None,
- lock_retention_period_years=None, daemon_name=None):
+ lock_retention_period_years=None, tags=None, daemon_name=None):
encryption_state = str_to_bool(encryption_state)
# When linking a non-tenant-user owned bucket to a tenanted user, we
# need to prefix bucket name with '/'. e.g. photos -> /photos
self._set_encryption(bucket_name, encryption_type, key_id, daemon_name, uid)
if encryption_status['Status'] == 'Enabled' and (not encryption_state):
self._delete_encryption(bucket_name, daemon_name, uid)
+ if tags:
+ self._set_tags(bucket_name, tags, daemon_name, uid)
return self._append_bid(result)
def delete(self, bucket, purge_objects='true', daemon_name=None):
--- /dev/null
+<cd-modal [modalRef]="activeModal">
+ <span class="modal-title"
+ i18n>{{ getMode() }} Tag</span>
+
+ <ng-container class="modal-content">
+ <form class="form"
+ #formDir="ngForm"
+ [formGroup]="form">
+ <div class="modal-body">
+ <!-- Key -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="key"
+ i18n>Key</label>
+ <div class="cd-col-form-input">
+ <input type="text"
+ class="form-control"
+ formControlName="key"
+ id="key">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('key', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('key', formDir, 'unique')"
+ i18n>This key must be unique.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('key', formDir, 'maxLength')"
+ i18n>Length of the key must be maximum of 128 characters</span>
+ </div>
+ </div>
+
+ <!-- Value -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="value"
+ i18n>Value</label>
+ <div class="cd-col-form-input">
+ <input id="value"
+ class="form-control"
+ type="text"
+ formControlName="value">
+ <span *ngIf="form.showError('value', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('value', formDir, 'maxLength')"
+ i18n>Length of the value must be a maximum of 128 characters</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="form"
+ [submitText]="getMode()"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+ </cd-modal>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BucketTagModalComponent } from './bucket-tag-modal.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+describe('BucketTagModalComponent', () => {
+ let component: BucketTagModalComponent;
+ let fixture: ComponentFixture<BucketTagModalComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [BucketTagModalComponent],
+ imports: [HttpClientTestingModule, ReactiveFormsModule],
+ providers: [NgbActiveModal]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(BucketTagModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, EventEmitter, Output } from '@angular/core';
+import { Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+
+@Component({
+ selector: 'cd-bucket-tag-modal',
+ templateUrl: './bucket-tag-modal.component.html',
+ styleUrls: ['./bucket-tag-modal.component.scss']
+})
+export class BucketTagModalComponent {
+ @Output()
+ submitAction = new EventEmitter();
+
+ form: CdFormGroup;
+ editMode = false;
+ currentKeyTags: string[];
+ storedKey: string;
+
+ constructor(
+ private formBuilder: CdFormBuilder,
+ public activeModal: NgbActiveModal,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ this.createForm();
+ }
+
+ private createForm() {
+ this.form = this.formBuilder.group({
+ key: [
+ null,
+ [
+ Validators.required,
+ CdValidators.custom('unique', (value: string) => {
+ if (_.isEmpty(value) && !this.currentKeyTags) {
+ return false;
+ }
+ return this.storedKey !== value && this.currentKeyTags.includes(value);
+ }),
+ CdValidators.custom('maxLength', (value: string) => {
+ if (_.isEmpty(value)) return false;
+ return value.length > 128;
+ })
+ ]
+ ],
+ value: [
+ null,
+ [
+ Validators.required,
+ CdValidators.custom('maxLength', (value: string) => {
+ if (_.isEmpty(value)) return false;
+ return value.length > 128;
+ })
+ ]
+ ]
+ });
+ }
+
+ onSubmit() {
+ this.submitAction.emit(this.form.value);
+ this.activeModal.close();
+ }
+
+ getMode() {
+ return this.editMode ? this.actionLabels.EDIT : this.actionLabels.ADD;
+ }
+
+ fillForm(tag: Record<string, string>) {
+ this.form.setValue(tag);
+ }
+}
</ng-container>
</tbody>
</table>
+
+ <!-- Tags -->
+ <ng-container *ngIf="selection.tagset">
+ <legend i18n>Tags</legend>
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr *ngFor="let tag of selection.tagset | keyvalue">
+ <td i18n
+ class="bold w-25">{{tag.key}}</td>
+ <td class="w-75">{{ tag.value }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </ng-container>
+
</ng-template>
</ng-container>
</div>
</fieldset>
+ <!-- Tags -->
+ <legend class="cd-header"
+ i18n>Tags
+ <cd-helper>Tagging gives you a way to categorize storage</cd-helper>
+ </legend>
+ <span *ngFor="let tag of tags; let i=index;">
+ <ng-container *ngTemplateOutlet="tagTpl; context:{index: i, tag: tag}"></ng-container>
+ </span>
+
+ <div class="row">
+ <div class="col-12">
+ <strong *ngIf="tags.length > 19"
+ class="text-warning"
+ i18n>Maximum of 20 tags reached</strong>
+ <button type="button"
+ id="add-tag"
+ class="btn btn-light float-end my-3"
+ [disabled]="tags.length > 19"
+ (click)="showTagModal()">
+ <i [ngClass]="[icons.add]"></i>
+ <ng-container i18n>Add tag</ng-container>
+ </button>
+ </div>
+ </div>
+
+
</div>
<div class="card-footer">
<cd-form-button-panel (submitActionEvent)="submit()"
</div>
</form>
</div>
+
+<ng-template #tagTpl
+ let-tag="tag"
+ let-index="index">
+ <div class="input-group my-2">
+ <ng-container *ngFor="let config of tagConfig">
+ <input type="text"
+ id="tag-{{config.attribute}}-{{index}}"
+ class="form-control"
+ [ngbTooltip]="config.attribute"
+ [value]="tag[config.attribute]"
+ disabled
+ readonly>
+ </ng-container>
+
+ <!-- Tag actions -->
+ <button type="button"
+ class="btn btn-light"
+ id="tag-edit-{{index}}"
+ i18n-ngbTooltip
+ ngbTooltip="Edit"
+ (click)="showTagModal(index)">
+ <i [ngClass]="[icons.edit]"></i>
+ </button>
+ <button type="button"
+ class="btn btn-light"
+ id="tag-delete-{{index}}"
+ i18n-ngbTooltip
+ ngbTooltip="Delete"
+ (click)="deleteTag(index)">
+ <i [ngClass]="[icons.trash]"></i>
+ </button>
+ </div>
+</ng-template>
import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
import { RgwConfigModalComponent } from '../rgw-config-modal/rgw-config-modal.component';
+import { BucketTagModalComponent } from '../bucket-tag-modal/bucket-tag-modal.component';
@Component({
selector: 'cd-rgw-bucket-form',
icons = Icons;
kmsVaultConfig = false;
s3VaultConfig = false;
+ tags: Record<string, string>[] = [];
+ tagConfig = [
+ {
+ attribute: 'key'
+ },
+ {
+ attribute: 'value'
+ }
+ ];
get isVersioningEnabled(): boolean {
return this.bucketForm.getValue('versioning');
value['versioning'] = bidResp['versioning'] === RgwBucketVersioning.ENABLED;
value['mfa-delete'] = bidResp['mfa_delete'] === RgwBucketMfaDelete.ENABLED;
value['encryption_enabled'] = bidResp['encryption'] === 'Enabled';
+ if (bidResp['tagset']) {
+ for (const [key, value] of Object.entries(bidResp['tagset'])) {
+ this.tags.push({ key: key, value: value.toString() });
+ }
+ }
// Append default values.
value = _.merge(defaults, value);
// Update the form.
return;
}
const values = this.bucketForm.value;
+ const xmlStrTags = this.tagsToXML(this.tags);
if (this.editing) {
// Edit
const versioning = this.getVersioningStatus();
values['mfa-token-serial'],
values['mfa-token-pin'],
values['lock_mode'],
- values['lock_retention_period_days']
+ values['lock_retention_period_days'],
+ xmlStrTags
)
.subscribe(
() => {
values['lock_retention_period_days'],
values['encryption_enabled'],
values['encryption_type'],
- values['keyId']
+ values['keyId'],
+ xmlStrTags
)
.subscribe(
() => {
.get('encryptionType')
.setValue(this.bucketForm.getValue('encryption_type') || 'AES256');
}
+
+ showTagModal(index?: number) {
+ const modalRef = this.modalService.show(BucketTagModalComponent);
+ const modalComponent = modalRef.componentInstance as BucketTagModalComponent;
+ modalComponent.currentKeyTags = this.tags.map((item) => item.key);
+
+ if (_.isNumber(index)) {
+ modalComponent.editMode = true;
+ modalComponent.fillForm(this.tags[index]);
+ modalComponent.storedKey = this.tags[index]['key'];
+ }
+
+ modalComponent.submitAction.subscribe((tag: Record<string, string>) => {
+ this.setTag(tag, index);
+ });
+ }
+
+ deleteTag(index: number) {
+ this.tags.splice(index, 1);
+ }
+
+ private setTag(tag: Record<string, string>, index?: number) {
+ if (_.isNumber(index)) {
+ this.tags[index] = tag;
+ } else {
+ this.tags.push(tag);
+ }
+ this.bucketForm.markAsDirty();
+ this.bucketForm.updateValueAndValidity();
+ }
+
+ private tagsToXML(tags: Record<string, string>[]): string {
+ let xml = '<Tagging><TagSet>';
+ for (const tag of tags) {
+ xml += '<Tag>';
+ for (const key in tag) {
+ if (key === 'key') {
+ xml += `<Key>${tag[key]}</Key>`;
+ } else if (key === 'value') {
+ xml += `<Value>${tag[key]}</Value>`;
+ }
+ }
+ xml += '</Tag>';
+ }
+ xml += '</TagSet></Tagging>';
+ return xml;
+ }
}
import { RgwSyncPrimaryZoneComponent } from './rgw-sync-primary-zone/rgw-sync-primary-zone.component';
import { RgwSyncMetadataInfoComponent } from './rgw-sync-metadata-info/rgw-sync-metadata-info.component';
import { RgwSyncDataInfoComponent } from './rgw-sync-data-info/rgw-sync-data-info.component';
+import { BucketTagModalComponent } from './bucket-tag-modal/bucket-tag-modal.component';
@NgModule({
imports: [
RgwOverviewDashboardComponent,
RgwSyncPrimaryZoneComponent,
RgwSyncMetadataInfoComponent,
- RgwSyncDataInfoComponent
+ RgwSyncDataInfoComponent,
+ BucketTagModalComponent
]
})
export class RgwModule {}
'5',
true,
'aws:kms',
- 'qwerty1'
+ 'qwerty1',
+ null
)
.subscribe();
const req = httpTesting.expectOne(
- `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&${RgwHelper.DAEMON_QUERY_PARAM}`
+ `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&tags=null&${RgwHelper.DAEMON_QUERY_PARAM}`
);
expect(req.request.method).toBe('POST');
});
'1',
'223344',
'GOVERNANCE',
- '10'
+ '10',
+ null
)
.subscribe();
const req = httpTesting.expectOne(
- `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`
+ `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`
);
expect(req.request.method).toBe('PUT');
});
lock_retention_period_days: string,
encryption_state: boolean,
encryption_type: string,
- key_id: string
+ key_id: string,
+ tags: string
) {
return this.rgwDaemonService.request((params: HttpParams) => {
return this.http.post(this.url, null, {
encryption_state: String(encryption_state),
encryption_type,
key_id,
+ tags: tags,
daemon_name: params.get('daemon_name')
}
})
mfaTokenSerial: string,
mfaTokenPin: string,
lockMode: 'GOVERNANCE' | 'COMPLIANCE',
- lockRetentionPeriodDays: string
+ lockRetentionPeriodDays: string,
+ tags: string
) {
return this.rgwDaemonService.request((params: HttpParams) => {
params = params.appendAll({
mfa_token_serial: mfaTokenSerial,
mfa_token_pin: mfaTokenPin,
lock_mode: lockMode,
- lock_retention_period_days: lockRetentionPeriodDays
+ lock_retention_period_days: lockRetentionPeriodDays,
+ tags: tags
});
return this.http.put(`${this.url}/${bucket}`, null, { params: params });
});
type: string
placement_target:
type: string
+ tags:
+ type: string
uid:
type: string
zonegroup:
type: string
mfa_token_serial:
type: string
+ tags:
+ type: string
uid:
type: string
versioning_state:
except RequestException as e:
raise DashboardException(msg=str(e), component='rgw')
+ @RestClient.api_put('/{bucket_name}?tagging')
+ def set_tags(self, bucket_name, tags, request=None):
+ # pylint: disable=unused-argument
+ try:
+ ET.fromstring(tags)
+ except ET.ParseError:
+ return "Data must be properly formatted"
+ try:
+ result = request(data=tags) # type: ignore
+ except RequestException as e:
+ raise DashboardException(msg=str(e), component='rgw')
+ return result
+
@RestClient.api_get('/{bucket_name}?object-lock')
def get_bucket_locking(self, bucket_name, request=None):
# type: (str, Optional[object]) -> dict