Add create new config item

Allows users to create a new config type (i.e. manifest, context,
encryption config, etc.) from the UI and save changes to
airship config.

Known issues:
- Still no validation on frontend forms yet
- Validation on backend is still hit or miss. It's still possible
  to make changes that render the config file invalid, and they
  must be fixed with text editor
- Some config keys are deprecated since the airshipctl version
  hasn't been uplifted in some time. That's a big todo...

Change-Id: Ifeefe26933966d0a434d1346ea677c213d976b78
This commit is contained in:
Matthew Fuller 2020-11-09 21:33:07 +00:00
parent c004a202dc
commit b20f7feba4
16 changed files with 337 additions and 53 deletions

View File

@ -1,7 +1,7 @@
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title class="title">
<h4>{{config.name}}</h4>
<h4>{{config.Name}}</h4>
</mat-panel-title>
</mat-expansion-panel-header>
<p>

View File

@ -42,7 +42,7 @@ export class ConfigManagementComponent implements OnInit {
constructor(private websocketService: WsService) { }
ngOnInit(): void {
this.name.setValue(this.config.name);
this.name.setValue(this.config.Name);
this.insecure.setValue(this.config.insecure);
this.systemActionRetries.setValue(this.config.systemActionRetries);
this.systemRebootDelay.setValue(this.config.systemRebootDelay);
@ -67,7 +67,7 @@ export class ConfigManagementComponent implements OnInit {
msg.name = this.name.value;
const cfg: ManagementConfig = {
name: this.name.value,
Name: this.name.value,
insecure: this.insecure.value,
// TODO(mfuller): need to validate these are numerical values in the form
systemActionRetries: +this.systemActionRetries.value,

View File

@ -0,0 +1,21 @@
/*
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
*/
.form-content {
overflow-y: auto;
}
.text-input {
width: 80%;
}

View File

@ -0,0 +1,18 @@
<h1 mat-dialog-title>New {{data.formName}} configuration</h1>
<div mat-dialog-content class="form-content">
<form [formGroup]="group">
<div *ngFor="let key of keys">
<mat-form-field *ngIf="!isBool(dataObj[key])" appearance="fill">
<mat-label>{{key}}</mat-label>
<input class="text-input" formControlName="{{key}}" matInput>
</mat-form-field>
<p *ngIf="isBool(dataObj[key])">
<mat-checkbox formControlName="{{key}}" labelPosition="before">{{key}} </mat-checkbox>
</p>
</div>
</form>
</div>
<div mat-dialog-actions>
<button mat-raised-button (click)="closeDialog()">Cancel</button>
<button mat-raised-button color="primary" (click)="setConfig(data.formName)">Save</button>
</div>

View File

@ -0,0 +1,61 @@
/*
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { ConfigNewComponent } from './config-new.component';
describe('ConfigNewComponent', () => {
let component: ConfigNewComponent;
let fixture: ComponentFixture<ConfigNewComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
MatButtonModule,
MatInputModule,
MatDialogModule,
MatCheckboxModule,
ToastrModule.forRoot(),
],
declarations: [ ConfigNewComponent ],
providers: [
{provide: MAT_DIALOG_DATA, useValue: {formType: 'context'}},
{provide: MatDialogRef, useValue: {}}
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ConfigNewComponent);
component = fixture.componentInstance;
component.data.formName = 'context';
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,103 @@
/*
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
*/
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { WsService } from 'src/services/ws/ws.service';
import { FormControl } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ContextOptions, EncryptionConfigOptions, ManagementConfig, ManifestOptions } from '../config.models';
import { WsConstants, WsMessage } from 'src/services/ws/ws.models';
@Component({
selector: 'app-config-new',
templateUrl: './config-new.component.html',
styleUrls: ['./config-new.component.css']
})
export class ConfigNewComponent implements OnInit {
group: FormGroup;
dataObj: any;
keys: string[] = [];
dataObjs = {
context: new ContextOptions(),
manifest: new ManifestOptions(),
encryption: new EncryptionConfigOptions(),
management: new ManagementConfig()
};
constructor(private websocketService: WsService,
private fb: FormBuilder,
@Inject(MAT_DIALOG_DATA) public data: {formName: string},
public dialogRef: MatDialogRef<ConfigNewComponent>) { }
ngOnInit(): void {
const grp = {};
this.dataObj = this.dataObjs[this.data.formName];
for (const [key, val] of Object.entries(this.dataObj)) {
this.keys.push(key);
grp[key] = new FormControl(val);
}
this.group = new FormGroup(grp);
}
setConfig(type: string): void {
let subComponent = '';
switch (type) {
case 'context':
subComponent = WsConstants.SET_CONTEXT;
break;
case 'manifest':
subComponent = WsConstants.SET_MANIFEST;
break;
case 'encryption':
subComponent = WsConstants.SET_ENCRYPTION_CONFIG;
break;
case 'management':
subComponent = WsConstants.SET_MANAGEMENT_CONFIG;
break;
}
for (const [key, control] of Object.entries(this.group.controls)) {
// TODO(mfuller): need to validate this within the form
if (typeof this.dataObj[key] === 'number') {
this.dataObj[key] = +control.value;
} else {
this.dataObj[key] = control.value;
}
}
const msg = new WsMessage(WsConstants.CTL, WsConstants.CONFIG, subComponent);
msg.data = JSON.parse(JSON.stringify(this.dataObj));
msg.name = this.dataObj.Name;
this.websocketService.sendMessage(msg);
this.dialogRef.close();
}
closeDialog(): void {
this.dialogRef.close();
}
// annoying helper method because apparently I can't just test this natively
// inside an *ngIf
isBool(val: any): boolean {
return typeof val === 'boolean';
}
}

View File

@ -0,0 +1,43 @@
/*
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
*/
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input';
import { ContextOptions, EncryptionConfigOptions, ManagementConfig, ManifestOptions } from '../config.models';
@NgModule({
imports: [
CommonModule,
FormsModule,
MatInputModule,
MatButtonModule,
ReactiveFormsModule,
MatCheckboxModule,
MatDialogModule,
ContextOptions,
ManifestOptions,
ManagementConfig,
EncryptionConfigOptions
],
declarations: [
],
providers: []
})
export class ConfigNewModule { }

View File

@ -14,7 +14,11 @@
<mat-accordion *ngFor="let context of contexts">
<app-config-context [context]="context"></app-config-context>
</mat-accordion>
<button mat-icon-button (click)="newConfig('context')">
<mat-icon class="grey-icon" svgIcon="add"></mat-icon>New Context
</button>
</mat-expansion-panel>
<br />
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title class="title">
@ -24,7 +28,11 @@
<mat-accordion *ngFor="let manifest of manifests">
<app-config-manifest [manifest]="manifest"></app-config-manifest>
</mat-accordion>
<button mat-icon-button (click)="newConfig('manifest')">
<mat-icon class="grey-icon" svgIcon="add"></mat-icon>New Manifest
</button>
</mat-expansion-panel>
<br />
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title class="title">
@ -34,7 +42,11 @@
<mat-accordion *ngFor="let config of encryptionConfigs">
<app-config-encryption [config]="config"></app-config-encryption>
</mat-accordion>
<button mat-icon-button (click)="newConfig('encryption')">
<mat-icon class="grey-icon" svgIcon="add"></mat-icon>New Encryption Config
</button>
</mat-expansion-panel>
<br />
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title class="title">
@ -44,6 +56,9 @@
<mat-accordion *ngFor="let config of managementConfigs">
<app-config-management [config]="config"></app-config-management>
</mat-accordion>
<button mat-icon-button (click)="newConfig('management')">
<mat-icon class="grey-icon" svgIcon="add"></mat-icon>New Management Config
</button>
</mat-expansion-panel>
</div>
<ng-template #initblock>

View File

@ -30,6 +30,7 @@ import { ConfigEncryptionComponent } from './config-encryption/config-encryption
import { ConfigContextComponent } from './config-context/config-context.component';
import { MatExpansionModule } from '@angular/material/expansion';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatDialogModule } from '@angular/material/dialog';
describe('ConfigComponent', () => {
let component: ConfigComponent;
@ -49,7 +50,8 @@ describe('ConfigComponent', () => {
ConfigEncryptionModule,
ReactiveFormsModule,
MatExpansionModule,
BrowserAnimationsModule
BrowserAnimationsModule,
MatDialogModule
],
declarations: [
ConfigComponent,

View File

@ -18,6 +18,8 @@ import { LogMessage } from 'src/services/log/log-message';
import { Context, ManagementConfig, Manifest, EncryptionConfig } from './config.models';
import { WsService } from 'src/services/ws/ws.service';
import { WsMessage, WsReceiver, WsConstants } from 'src/services/ws/ws.models';
import { MatDialog } from '@angular/material/dialog';
import { ConfigNewComponent } from './config-new/config-new.component';
@Component({
selector: 'app-bare-metal',
@ -37,7 +39,8 @@ export class ConfigComponent implements WsReceiver, OnInit {
managementConfigs: ManagementConfig[] = [];
encryptionConfigs: EncryptionConfig[] = [];
constructor(private websocketService: WsService) {
constructor(private websocketService: WsService,
public dialog: MatDialog) {
this.websocketService.registerFunctions(this);
}
@ -81,15 +84,19 @@ export class ConfigComponent implements WsReceiver, OnInit {
break;
case WsConstants.SET_CONTEXT:
this.websocketService.printIfToast(message);
this.getContexts();
break;
case WsConstants.SET_ENCRYPTION_CONFIG:
this.websocketService.printIfToast(message);
this.getEncryptionConfigs();
break;
case WsConstants.SET_MANIFEST:
this.websocketService.printIfToast(message);
this.getManifests();
break;
case WsConstants.SET_MANAGEMENT_CONFIG:
this.websocketService.printIfToast(message);
this.getManagementConfigs();
break;
default:
Log.Error(new LogMessage('Config message sub component not handled', this.className, message));
@ -109,7 +116,7 @@ export class ConfigComponent implements WsReceiver, OnInit {
getAirshipConfigPath(): void {
this.websocketService.sendMessage(new WsMessage(
this.type, this.component, 'getAirshipConfigPath')
this.type, this.component, WsConstants.GET_AIRSHIP_CONFIG_PATH)
);
}
@ -142,4 +149,12 @@ export class ConfigComponent implements WsReceiver, OnInit {
this.type, this.component, WsConstants.GET_MANAGEMENT_CONFIGS)
);
}
newConfig(configType: string): void {
const dialogRef = this.dialog.open(ConfigNewComponent, {
width: '550px',
height: '650px',
data: { formName: configType}
});
}
}

View File

@ -29,19 +29,23 @@ export class Context {
}
export class ContextOptions {
Name: string;
Manifest: string;
ManagementConfiguration: string;
EncryptionConfig: string;
Name = '';
Manifest = '';
ManagementConfiguration = '';
EncryptionConfig = '';
}
// There's no corresponding ManagementConfigOptions in CTL, so the
// Name property has been deliberately capitalized to make it consistent
// with the other ConfigOptions structs and we can retrieve it without
// special handling
export class ManagementConfig {
name: string;
insecure: boolean;
systemActionRetries: number;
systemRebootDelay: number;
type: string;
useproxy: boolean;
Name = '';
insecure = false;
systemActionRetries = 0;
systemRebootDelay = 0;
type = '';
useproxy = false;
}
export class Manifest {
@ -88,24 +92,24 @@ export class Permissions {
}
export class ManifestOptions {
Name: string;
RepoName: string;
URL: string;
Branch: string;
CommitHash: string;
Tag: string;
RemoteRef: string;
Force: boolean;
IsPhase: boolean;
SubPath: string;
TargetPath: string;
MetadataPath: string;
Name = '';
RepoName = '';
URL = '';
Branch = '';
CommitHash = '';
Tag = '';
RemoteRef = '';
Force = false;
IsPhase = false;
SubPath = '';
TargetPath = '';
MetadataPath = '';
}
export class EncryptionConfigOptions {
Name: string;
EncryptionKeyPath: string;
DecryptionKeyPath: string;
KeySecretName: string;
KeySecretNamespace: string;
Name = '';
EncryptionKeyPath = '';
DecryptionKeyPath = '';
KeySecretName = '';
KeySecretNamespace = '';
}

View File

@ -28,6 +28,7 @@ import { ConfigManifestComponent } from './config-manifest/config-manifest.compo
import { ConfigInitComponent } from './config-init/config-init.component';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatExpansionModule } from '@angular/material/expansion';
import { ConfigNewComponent } from './config-new/config-new.component';
@NgModule({
imports: [
@ -48,7 +49,8 @@ import { MatExpansionModule } from '@angular/material/expansion';
ConfigEncryptionComponent,
ConfigManagementComponent,
ConfigManifestComponent,
ConfigInitComponent
ConfigInitComponent,
ConfigNewComponent
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>

After

Width:  |  Height:  |  Size: 173 B

View File

@ -34,5 +34,6 @@ export enum Icons {
close = 'close',
lock = 'lock',
lock_open = 'lock_open',
history = 'history'
history = 'history',
add = 'add'
}

View File

@ -27,8 +27,8 @@ import (
)
const (
AirshipConfigNotFoundErr = `No airship config file found.
Please visit the Config section to specify or initialize a config file.`
// AirshipConfigNotFoundErr generic error for missing airship config file
AirshipConfigNotFoundErr = "No airship config file found."
)
// CTLFunctionMap is a function map for the CTL functions that is referenced in the webservice

View File

@ -226,7 +226,7 @@ func GetManifests(request configs.WsMessage) configs.WsMessage {
// ManagementConfig wrapper struct for CTL's ManagementConfiguration that
// includes a name
type ManagementConfig struct {
Name string `json:"name"`
Name string `json:"Name"`
ctlconfig.ManagementConfiguration
}
@ -408,22 +408,20 @@ func SetManagementConfig(request configs.WsMessage) configs.WsMessage {
return response
}
if mCfg, found := client.Config.ManagementConfiguration[request.Name]; found {
err = json.Unmarshal(bytes, mCfg)
if err != nil {
e := err.Error()
response.Error = &e
return response
}
var mCfg ctlconfig.ManagementConfiguration
err = client.Config.PersistConfig()
if err != nil {
e := err.Error()
response.Error = &e
return response
}
} else {
e := fmt.Sprintf("Management configuration '%s' not found", request.Name)
err = json.Unmarshal(bytes, &mCfg)
if err != nil {
e := err.Error()
response.Error = &e
return response
}
client.Config.ManagementConfiguration[request.Name] = &mCfg
err = client.Config.PersistConfig()
if err != nil {
e := err.Error()
response.Error = &e
return response
}