Skip to content

Commit

Permalink
Add cloud-init & Webservices to Admin-UI (#149)
Browse files Browse the repository at this point in the history
* WIP: edit webinterfaces in own component

* reafctor to vm template service comfiguration/form

* fix json parse/stringify for vm-template-services

* remove unused component, add get funcion for config string

* refactor object to map

---------

Co-authored-by: Philip Prinz <Philip.Prinz@sva.de>
  • Loading branch information
jggoebel and Philip Prinz authored Feb 10, 2023
1 parent d475483 commit eecc52f
Show file tree
Hide file tree
Showing 13 changed files with 408 additions and 21 deletions.
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import { NewRoleBindingComponent } from './user/new-role-binding/new-role-bindin
import { DeleteProcessModalComponent } from './user/user/delete-process-modal/delete-process-modal.component';
import { EnvironmentDetailComponent } from './configuration/environments/environment-detail/environment-detail.component';
import { VmTemplateDetailComponent } from './configuration/vmtemplates/vmtemplate-detail/vmtemplate-detail.component';
import { VMTemplateServiceFormComponent } from './configuration/vmtemplates/edit-vmtemplate/vmtemplate-service-form/vmtemplate-service-form.component';
import { FilterScenariosComponent } from './filter-scenarios/filter-scenarios.component';

const appInitializerFn = (appConfig: AppConfigService) => {
Expand Down Expand Up @@ -146,6 +147,7 @@ export function jwtOptionsFactory() {
NewRoleBindingComponent,
EnvironmentDetailComponent,
VmTemplateDetailComponent,
VMTemplateServiceFormComponent,
FilterScenariosComponent
],
imports: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<clr-wizard #wizard clrWizardSize="xl" [clrWizardDisableStepnav]="false">
<clr-wizard-title>Edit VM Template</clr-wizard-title>

<clr-wizard-button [clrWizardButtonDisabled]="buttonsDisabled" [type]="'cancel'">Cancel</clr-wizard-button>
<clr-wizard-button [clrWizardButtonDisabled]="buttonsDisabled" [type]="'previous'">Previous</clr-wizard-button>
<clr-wizard-button [clrWizardButtonDisabled]="buttonsDisabled" [type]="'next'">Next</clr-wizard-button>
Expand All @@ -12,15 +12,14 @@
<form clrForm [formGroup]="templateDetails">
<clr-input-container>
<label>Name</label>
<input clrInput type="text" placeholder="name" name="name"
formControlName="name" required />
<input clrInput type="text" placeholder="name" name="name" formControlName="name" required />
<clr-control-error *clrIfError="'required'">Template name is required</clr-control-error>
<clr-control-error *clrIfError="'minlength'">Template name must be longer than 4 characters</clr-control-error>
<clr-control-error *clrIfError="'minlength'">Template name must be longer than 4
characters</clr-control-error>
</clr-input-container>
<clr-input-container>
<label>Image</label>
<input clrInput type="text" placeholder="image" name="image"
formControlName="image" required />
<input clrInput type="text" placeholder="image" name="image" formControlName="image" required />
<clr-control-error *clrIfError="'required'">Image is required</clr-control-error>
</clr-input-container>
</form>
Expand Down Expand Up @@ -62,10 +61,41 @@
</tbody>
</table>
<ng-container *ngIf="configMap && !configMap.valid">
<span class="clr-subtext">All mappings must have key and value filled out. Complete, or remove, any entries that do not.</span>
<span class="clr-subtext">All mappings must have key and value filled out. Complete, or remove, any entries
that do not.</span>
</ng-container>
</clr-wizard-page>
<clr-wizard-page (clrWizardPageOnLoad)="copyTemplate()" [clrWizardPagePreventDefaultNext]="true" (clrWizardPageFinish)="saveTemplate()">

<clr-wizard-page>
<ng-template clrPageTitle>Services</ng-template>
<app-vmtemplate-service-form
[cloudConfig]="cloudConfig"
></app-vmtemplate-service-form>
</clr-wizard-page>

<clr-wizard-page [clrWizardPageNextDisabled]="!configMap.valid">
<ng-template clrPageTitle>Cloud Config</ng-template>
<clr-signpost>
<clr-signpost-content *clrIfOpen>
<h3>Cloud Config</h3>
<p>This is the resulting Cloud Config from the previous step. It is put together based on the Order in which the Services are put in the List</p>
</clr-signpost-content>
</clr-signpost>
<div [formGroup]="configMap">
<form clrForm style="width: 100%;">
<div style="background-color: var(--clr-thead-bgcolor, #fafafa); width: fit-content;padding: 9px; border: 1px solid var(--clr-table-border-color, #cccccc);">
<code class="clr-code" style="white-space: pre; color: var(--clr-table-font-color, #666666);">{{ cloudConfig.cloudConfigYaml }}</code>
</div>
</form>
</div>
<ng-container *ngIf="configMap && !configMap.valid">
<span class="clr-subtext">All mappings must have key and value filled out. Complete, or remove, any entries
that do not.</span>
</ng-container>
</clr-wizard-page>

<clr-wizard-page (clrWizardPageOnLoad)="copyTemplate()" [clrWizardPagePreventDefaultNext]="true"
(clrWizardPageFinish)="saveTemplate()">
<ng-template clrPageTitle>Confirmation</ng-template>
<alert #alert></alert>
<p>Confirm the following details before finalizing</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild }
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ClrWizard } from '@clr/angular';
import { AlertComponent } from 'src/app/alert/alert.component';
import { CloudInitConfig } from 'src/app/data/cloud-init-config';
import { ServerResponse } from 'src/app/data/serverresponse';
import { vmServiceToJSON } from 'src/app/data/vm-template-service-configuration';
import { VMTemplate } from 'src/app/data/vmtemplate';
import { VmtemplateService } from 'src/app/data/vmtemplate.service';

Expand All @@ -16,13 +18,21 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
public configMap: FormGroup;
public buttonsDisabled: boolean = false;

private cloudConfigKey: string = 'cloud-config'
private vmServiceKey: string = 'webinterfaces'
public cloudConfig: CloudInitConfig = new CloudInitConfig();

@Input()
public editTemplate: VMTemplate;
public template: VMTemplate = new VMTemplate();
public editTemplate: VMTemplate;

@Output()
public event: EventEmitter<boolean> = new EventEmitter(false);

public template: VMTemplate = new VMTemplate();
public selectWebinterfaceModalOpen: boolean = false
public newWebinterfaceModalOpen: boolean = false


constructor(
private _fb: FormBuilder,
private vmTemplateService: VmtemplateService
Expand Down Expand Up @@ -70,10 +80,25 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
})
}

private buildVMServices(configMapData?: string) {
if (configMapData) {
let temp = JSON.parse(configMapData)
let resultMap = new Map()
temp.forEach(entry => {
entry.cloudConfigMap = new Map(Object.entries(entry["cloudConfigMap"])); // Convert Object to map
resultMap.set(entry['name'], entry)
})
return resultMap
}
else return new Map()
}

public prepareConfigMap() {
// differs from buildConfigMap() in that we are copying existing values
// into the form
var configKeys = Object.keys(this.editTemplate.config_map)
let configKeys = Object.keys(this.editTemplate.config_map).filter(elem => elem !== this.cloudConfigKey && elem != this.vmServiceKey)
this.cloudConfig.vmServices = this.buildVMServices(this.editTemplate.config_map[this.vmServiceKey])
this.cloudConfig.buildNewYAMLFile()
this.configMap = this._fb.group({
mappings: this._fb.array([])
});
Expand All @@ -91,8 +116,8 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {

public newConfigMapping(key: string = '', value: string = '') {
var newGroup = this._fb.group({
key: [key, Validators.required],
value: [value, Validators.required]
key: [key, Validators.required],
value: [value, Validators.required]
});
(this.configMap.get('mappings') as FormArray).push(newGroup)
}
Expand All @@ -113,8 +138,15 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
var value = (this.configMap.get(['mappings', i]) as FormGroup).get('value').value
this.template.config_map[key] = value;
}
this.template.config_map[this.cloudConfigKey] = this.cloudConfig.cloudConfigYaml;
let tempArray = []
this.cloudConfig.vmServices.forEach(vmService => {
//tempArray.push(vmService);
tempArray.push(JSON.parse(vmServiceToJSON(vmService)))
})
let jsonString = JSON.stringify(tempArray)
this.template.config_map[this.vmServiceKey] = jsonString
}

public copyTemplate() {
this.copyConfigMap();
this.copyTemplateDetails();
Expand Down Expand Up @@ -168,4 +200,4 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
this.buildConfigMap();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<button class="btn btn-table btn-link" (click)="selectVMServiceModalOpen = true">
<clr-icon shape="plus"></clr-icon> Add predefined Webinterface
</button>
<button class="btn btn-table btn-link" (click)="openNewVMServiceModal()">
<clr-icon shape="plus"></clr-icon> Add new Service
</button>
<table class="table table-compact">
<th>Name</th>
<th>Port</th>
<th>Tab</th>
<th>Edit</th>
<th>Delete</th>
<tbody>
<tr *ngFor="let interface of cloudConfig.vmServices | keyvalue">
<td>{{ interface.value.name }}</td>
<td>{{ interface.value.port }}</td>
<td>{{ interface.value.hasOwnTab }}</td>
<td><button class="btn btn-table btn-link" (click)="editVMServiceClicked(interface.value)">EDIT</button>
</td>
<td><button class="btn btn-table btn-link"
(click)="cloudConfig.removeVMService(interface.key)">DELETE</button></td>
</tr>
</tbody>
</table>


<clr-modal [(clrModalOpen)]="selectVMServiceModalOpen" [clrModalSize]="'sm'">
<h3 class="modal-title">Choose a Service</h3>
<div class="modal-body">
<select clrSelect [(ngModel)]="selectedNewInterface">
<option *ngFor="let interface of predefinedInterfaces" [ngValue]="interface">{{ interface.name }}
</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="selectVMServiceModalOpen = false">Cancel</button>
<button type="button" class="btn btn-primary" (click)="selectModalClose()">Ok</button>
</div>
</clr-modal>

<clr-modal [(clrModalOpen)]="newVMServiceModalOpen">
<h3 class="modal-title">Create a new Sevice</h3>
<div class="modal-body">
<form clrForm [formGroup]="newVMServiceFormGroup">
<input clrInput placeholder="Name" name="name" formControlName="name" />
<clr-checkbox-container>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox value="hasWebinterface" name="hasWebinterface"
formControlName="hasWebinterface" />
<label>Has a Webinterface</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<ng-container *ngIf="newVMServiceFormGroup.value['hasWebinterface']">
<clr-checkbox-container>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox value="hasOwnTab" name="hasOwnTab"
formControlName="hasOwnTab" />
<label>Has it's own Tab in the UI</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<input type="number" clrInput placeholder="Port" name="input" formControlName="port" />
</ng-container>
<textarea clrTextarea style="width: 100%; height: 30vh; padding: 0;" name="cloudConfigString"
formControlName="cloudConfigString" required></textarea>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="newVMServiceModalOpen = false">Cancel</button>
<button type="button" class="btn btn-primary" (click)="newVMServiceClose()">Ok</button>
</div>
</clr-modal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { VMTemplateServiceFormComponent } from './vmtemplate-service-form.component';

describe('VMTemplateServiceFormComponent', () => {
let component: VMTemplateServiceFormComponent;
let fixture: ComponentFixture<VMTemplateServiceFormComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ VMTemplateServiceFormComponent ]
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(VMTemplateServiceFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { CloudInitConfig } from 'src/app/data/cloud-init-config';
import { getCloudConfigString, VMTemplateServiceConfiguration } from 'src/app/data/vm-template-service-configuration';

@Component({
selector: 'app-vmtemplate-service-form',
templateUrl: './vmtemplate-service-form.component.html',
styleUrls: ['./vmtemplate-service-form.component.scss']
})
export class VMTemplateServiceFormComponent implements OnInit {
public predefinedInterfaces: VMTemplateServiceConfiguration[] = PredefinedWebInterfaces //Placeholder until managed in Backend

@Input()
public cloudConfig: CloudInitConfig;


public selectedNewInterface: VMTemplateServiceConfiguration = undefined;
public newVMServiceFormGroup: FormGroup;
public editVMService: VMTemplateServiceConfiguration;

//Open/Close Modals
public editCloudConfigModalOpen: boolean = false
public selectVMServiceModalOpen: boolean = false
public newVMServiceModalOpen: boolean = false

constructor(
private fb: FormBuilder
) { }

ngOnInit(): void {
this.buildNewVMServiceDetails()
}

public buildNewVMServiceDetails(edit: boolean = false) {
this.newVMServiceFormGroup = this.fb.group({
name: [edit ? this.editVMService.name : ''],
port: [edit ? this.editVMService.port : undefined],
hasOwnTab: [edit ? this.editVMService.hasOwnTab : false],
cloudConfigString: [edit ? getCloudConfigString(this.editVMService) : ''],
hasWebinterface: [edit ? this.editVMService.hasWebinterface : false]
})
}


openCloudInitSelect() {
this.selectVMServiceModalOpen = true
}

openNewVMServiceModal() {
this.buildNewVMServiceDetails()
this.newVMServiceModalOpen = true
}


newVMServiceClose() {
let newVMService: VMTemplateServiceConfiguration = new VMTemplateServiceConfiguration()
newVMService.name = this.newVMServiceFormGroup.get('name').value;
newVMService.hasWebinterface = this.newVMServiceFormGroup.get('hasWebinterface').value;
newVMService.port = this.newVMServiceFormGroup.get('port').value;
newVMService.hasOwnTab = this.newVMServiceFormGroup.get('hasOwnTab').value;
newVMService.cloudConfigString = this.newVMServiceFormGroup.get('cloudConfigString').value;
newVMService.cloudConfigMap = this.cloudConfig.buildMapFromString(newVMService.cloudConfigString)
this.cloudConfig.addVMService(newVMService)
this.buildNewVMServiceDetails()
this.newVMServiceModalOpen = false
}

selectModalClose() {
this.selectedNewInterface.cloudConfigMap = this.cloudConfig.buildMapFromString(this.selectedNewInterface.cloudConfigString)
this.cloudConfig.addVMService(this.selectedNewInterface)
this.selectedNewInterface = undefined
this.selectVMServiceModalOpen = false
}

editVMServiceClicked(editVMService: VMTemplateServiceConfiguration) {
this.editVMService = editVMService;
this.buildNewVMServiceDetails(true)
this.newVMServiceModalOpen = true
}

}


export const PredefinedWebInterfaces: VMTemplateServiceConfiguration[] = //This has to be modeled into a CRD and retrieved over the Backend
[
{
name: "VS Code IDE", port: 8080, hasOwnTab: true, cloudConfigMap: new Map(), hasWebinterface: true,
cloudConfigString:
`#cloud-config
runcmd:
- export HOME=/root
- curl -fsSL https://code-server.dev/install.sh > codeServerInstall.sh
- /bin/sh codeServerInstall.sh && systemctl enable --now code-server@root
- |
sleep 5 && sed -i.bak 's/auth: password/auth: none/' ~/.config/code-server/config.yaml
- sudo systemctl restart code-server@root
`
},
{
name: "Test-Service without Webinterface", cloudConfigMap: new Map(), hasWebinterface: false,
cloudConfigString:
`#cloud-config
runcmd:
- touch /root/test.txt
`
},
{
name: "Test-Interface without Cloud Config", port: 8081, hasOwnTab: false, hasWebinterface: true,

}
]
Loading

0 comments on commit eecc52f

Please sign in to comment.