diff --git a/.gitignore b/.gitignore
index cf462ce1fd..22591504ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,6 +95,7 @@ src/jetstream/console-database.db
src/jetstream/config.properties
src/jetstream/db/dbconf.yml
src/jetstream/plugins/monocular/chart-repo/chartrepo
+src/jetstream/plugins/analysis/container/analyzers
# Customisations
diff --git a/custom-src/deploy/kubernetes/custom-build.sh b/custom-src/deploy/kubernetes/custom-build.sh
index b850a85563..9511d3412e 100644
--- a/custom-src/deploy/kubernetes/custom-build.sh
+++ b/custom-src/deploy/kubernetes/custom-build.sh
@@ -21,4 +21,9 @@ function custom_image_build() {
# Build and push an image for the Helm Repo Sync Tool
log "-- Building/publishing Monocular Chart Repo Sync Tool"
patchAndPushImage stratos-chartsync Dockerfile "${STRATOS_PATH}/src/jetstream/plugins/monocular/chart-repo"
+
+ # Analzyers container
+ log "-- Building/publishing Stratos Analyzers"
+ patchAndPushImage stratos-analyzers Dockerfile "${STRATOS_PATH}/src/jetstream/plugins/analysis/container"
+
}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.html
new file mode 100644
index 0000000000..4a0ede8deb
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.spec.ts
new file mode 100644
index 0000000000..d23be84d77
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.spec.ts
@@ -0,0 +1,38 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { MDAppModule } from './../../../../core/md.module';
+
+import { AnalysisReportSelectorComponent } from './analysis-report-selector.component';
+import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service';
+import { KubeBaseGuidMock, KubernetesBaseTestModules } from '../../kubernetes.testing.module';
+
+describe('AnalysisReportSelectorComponent', () => {
+ let component: AnalysisReportSelectorComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ AnalysisReportSelectorComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+ MDAppModule
+ ],
+ providers: [
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AnalysisReportSelectorComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.ts
new file mode 100644
index 0000000000..caf68a3561
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.ts
@@ -0,0 +1,83 @@
+import { Observable, Subject } from 'rxjs';
+import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
+import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service';
+import { map, first } from 'rxjs/operators';
+import * as moment from 'moment';
+
+@Component({
+ selector: 'app-analysis-report-selector',
+ templateUrl: './analysis-report-selector.component.html',
+ styleUrls: ['./analysis-report-selector.component.scss']
+})
+export class AnalysisReportSelectorComponent implements OnInit {
+
+ public selection = { title: 'None' };
+
+ public analyzers$ = new Subject();
+
+ @Input() endpoint;
+ @Input() path;
+ @Input() prompt = 'Overlay Analysis';
+ @Input() allowNone = true;
+ @Input() autoSelect;
+
+ @Output() selected = new EventEmitter();
+ @Output() reportCount = new EventEmitter();
+
+ autoSelected = false;
+
+ constructor(public analysisService: KubernetesAnalysisService) { }
+
+ ngOnInit() {
+ this.analyzers$.pipe(first()).subscribe(reports => {
+ // Auto-select first report
+ if (!this.autoSelected && this.autoSelect && reports.length > 0) {
+ this.onSelected(reports[0]);
+ }
+ });
+
+ this.fetchReports();
+ }
+
+ private fetchReports() {
+ this.analysisService.getByPath(this.endpoint, this.path).pipe(
+ map(d => {
+ const res = [];
+ if (this.allowNone) {
+ res.push({title: 'None'});
+ }
+ if (d) {
+ d.forEach(r => {
+ const c = {... r};
+ const title = c.type.substr(0, 1).toUpperCase() + c.type.substr(1);
+ const age = moment(c.created).fromNow(true);
+ c.title = `${title} (${age})`;
+ res.push(c);
+ });
+ }
+ this.reportCount.next(res.length);
+ return res;
+ })
+ ).subscribe(data => {
+ this.analyzers$.next(data);
+ });
+ }
+
+ // Selection changed
+ public onSelected(d) {
+ this.selection = d;
+ if (!d.id) {
+ this.selected.emit(null);
+ } else {
+ this.selected.next(d);
+ }
+ }
+
+ public refreshReports($event: MouseEvent) {
+ this.analysisService.refresh();
+ this.fetchReports();
+ $event.preventDefault();
+ $event.cancelBubble = true;
+ }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.html
new file mode 100644
index 0000000000..c527914e5a
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.html
@@ -0,0 +1 @@
+
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.spec.ts
new file mode 100644
index 0000000000..ed39ab0acf
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.spec.ts
@@ -0,0 +1,29 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AnalysisReportViewerComponent } from './analysis-report-viewer.component';
+import { KubernetesBaseTestModules } from '../kubernetes.testing.module';
+
+describe('AnalysisReportViewerComponent', () => {
+ let component: AnalysisReportViewerComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ AnalysisReportViewerComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AnalysisReportViewerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.ts
new file mode 100644
index 0000000000..b48f4eb268
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.ts
@@ -0,0 +1,86 @@
+import {
+ Component,
+ ComponentFactoryResolver,
+ ComponentRef,
+ Input,
+ OnDestroy,
+ OnInit,
+ Type,
+ ViewChild,
+ ViewContainerRef,
+} from '@angular/core';
+
+import { ClairReportViewerComponent } from './clair-report-viewer/clair-report-viewer.component';
+import { KubeScoreReportViewerComponent } from './kube-score-report-viewer/kube-score-report-viewer.component';
+import { PopeyeReportViewerComponent } from './popeye-report-viewer/popeye-report-viewer.component';
+
+export interface IReportViewer {
+ // setReport(report);
+ report: any;
+}
+
+@Component({
+ selector: 'app-analysis-report-viewer',
+ templateUrl: './analysis-report-viewer.component.html',
+ styleUrls: ['./analysis-report-viewer.component.scss']
+})
+export class AnalysisReportViewerComponent implements OnInit, OnDestroy {
+
+ // Component reference for the dynamically created auth form
+ @ViewChild('reportViewer', { read: ViewContainerRef, static: true })
+ public container: ViewContainerRef;
+ private reportComponentRef: ComponentRef;
+
+ private id: string;
+
+ @Input('report')
+ set report(report: any) {
+ if (report === null || report.id === this.id) {
+ return;
+ }
+ this.id = report.id;
+ this.updateReport(report);
+ }
+
+ constructor(
+ private resolver: ComponentFactoryResolver,
+ ) { }
+
+ ngOnInit() {
+ }
+
+ updateReport(report) {
+ switch (report.format) {
+ case 'popeye':
+ this.createComponent(PopeyeReportViewerComponent, report);
+ break;
+ case 'kubescore':
+ this.createComponent(KubeScoreReportViewerComponent, report);
+ break;
+ case 'clair':
+ this.createComponent(ClairReportViewerComponent, report);
+ break;
+ }
+ }
+
+ // Dynamically create the component for the report type type
+ createComponent(component: Type, report) {
+ if (!component || !this.container) {
+ return;
+ }
+
+ if (this.reportComponentRef) {
+ this.reportComponentRef.destroy();
+ }
+ const factory = this.resolver.resolveComponentFactory(component);
+ this.reportComponentRef = this.container.createComponent(factory);
+ // this.reportComponentRef.instance.setReport(report);
+ this.reportComponentRef.instance.report = report;
+ }
+
+ ngOnDestroy() {
+ if (this.reportComponentRef) {
+ this.reportComponentRef.destroy();
+ }
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.html
new file mode 100644
index 0000000000..5df3b1c1fa
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
{{ total }} vulnerabilities detected
+ 0">{{ patches }} have available patches
+
+
+
+
0" [dataSource]="dataSource" [columns]="columns">
+
+
+
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.scss
new file mode 100644
index 0000000000..88f9a33c37
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.scss
@@ -0,0 +1,64 @@
+
+.clair-detail {
+
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - 104px);
+
+ &__header {
+ align-items: center;
+ display: flex;
+ height: 40px;
+ font-size: 14px;
+ flex: 0 0 40px;
+ border-bottom: 1px solid #ccc;
+ background-color: #eee;
+ font-weight: bold;
+ opacity: 0.7;
+ padding: 0 10px;
+ }
+
+ &__header-label {
+ flex: 1;
+ }
+
+ &__header-count {
+ background-color: #ccc;
+ border-radius: 6px;
+ flex: 0;
+ font-size: 12px;
+ padding: 2px 10px;
+ }
+
+ &__main {
+ display: flex;
+ flex: 1;
+ height: calc(100vh - 144px);
+ overflow-y: auto;
+ }
+
+ &__inner {
+ width: 100%;
+ }
+
+ &__info {
+ padding: 10px;
+
+ h1 {
+ font-size: 18px;
+ }
+ h2 {
+ font-size: 16px;
+ }
+ }
+
+ &__info-panel {
+ display: flex;
+ flex-direction: row;
+
+ > div {
+ flex: 1;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.spec.ts
new file mode 100644
index 0000000000..b30081402c
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ClairReportDetailComponent } from './clair-report-detail.component';
+
+describe('ClairReportDetailComponent', () => {
+ let component: ClairReportDetailComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ClairReportDetailComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ClairReportDetailComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.ts
new file mode 100644
index 0000000000..b7cf663fbd
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component.ts
@@ -0,0 +1,142 @@
+import { Component, Input } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
+
+import { ITableColumn } from '../../../../../shared/components/list/list-table/table.types';
+import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service';
+import { ClairReportSeverityTableComponent } from '../clair-report-severity-table/clair-report-severity-table.component';
+import { ClairSeverityOrder, Vulnerabilities, Vulnerability } from './../clair-report.types';
+
+
+@Component({
+ selector: 'app-clair-report-detail',
+ templateUrl: './clair-report-detail.component.html',
+ styleUrls: ['./clair-report-detail.component.scss']
+})
+export class ClairReportDetailComponent {
+
+ imageReport: any;
+
+ // Report detail loaded from the analysis server
+ detail: Vulnerability[];
+
+ busy = new BehaviorSubject(true);
+
+ public dataSource: any;
+
+ public columns: ITableColumn[] = [];
+
+ data = new BehaviorSubject([]);
+
+ total = 0;
+ patches = 0;
+
+ // Totals as a map by severity
+ totals = {};
+
+ @Input() id: string;
+
+ @Input()
+ set report(data: any) {
+ this.imageReport = data;
+
+ // Feth the data
+ this.fetch();
+ }
+
+ get report() {
+ return this.imageReport;
+ }
+
+ constructor(public analysisServer: KubernetesAnalysisService) {
+
+ // Table columns
+ this.columns = [
+ {
+ columnId: 'name', headerCell: () => 'CVE',
+ cellDefinition: {
+ valuePath: 'Name'
+ },
+ cellFlex: '2',
+ },
+ {
+ columnId: 'severity', headerCell: () => 'Severity',
+ // cellDefinition: {
+ // valuePath: 'Severity'
+ // },
+ cellComponent: ClairReportSeverityTableComponent,
+ cellFlex: '2',
+ },
+ {
+ columnId: 'package', headerCell: () => 'Package',
+ cellDefinition: {
+ valuePath: 'FeatureName'
+ },
+ cellFlex: '2',
+ },
+ {
+ columnId: 'current_version', headerCell: () => 'Current Version',
+ cellDefinition: {
+ valuePath: 'FeatureVersion'
+ },
+ cellFlex: '2',
+ },
+ {
+ columnId: 'fixed_version', headerCell: () => 'Fixed In Version',
+ cellDefinition: {
+ valuePath: 'FixedBy'
+ },
+ cellFlex: '2',
+ },
+ ];
+
+ // Data source
+ this.dataSource = {
+ connect: () => this.data.asObservable(),
+ disconnect: () => { },
+ trackBy: (a, b) => {
+ return b.Name;
+ },
+ isTableLoading$: this.busy.asObservable(),
+ // getRowState: (row: any, schemaKey: string): Observable => {
+ // return observableOf({});
+ // }
+ };
+ }
+
+ private fetch() {
+ this.busy.next(true);
+
+ this.analysisServer.getReportFile(this.id, this.imageReport.details).subscribe(report => {
+
+ // Report always has the report envelope - the clair report is in the .report
+ if (report.report && report.report.Vulnerabilities) {
+ const klar: Vulnerabilities = report.report.Vulnerabilities;
+
+ const vulns: Vulnerability[] = [];
+ this.totals = {};
+ // Turn the map into a single list of vulnerabilities
+ ClairSeverityOrder.forEach(severity => {
+ if (klar[severity]) {
+ vulns.push(...klar[severity]);
+ this.totals[severity] = klar[severity].length;
+ }
+ });
+
+ this.patches = 0;
+ vulns.forEach(v => {
+ if (v.FixedBy) {
+ this.patches++;
+ }
+ });
+
+ this.detail = vulns;
+ this.data.next(vulns);
+ this.total = vulns.length;
+ this.busy.next(false);
+ }
+
+ });
+
+ }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.html
new file mode 100644
index 0000000000..c704715572
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.html
@@ -0,0 +1,7 @@
+
+
+
warning
+
{{ severityTotals[severity] }}
+
{{ severity }}
+
+
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.scss
new file mode 100644
index 0000000000..ef800d6bc2
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.scss
@@ -0,0 +1,44 @@
+.clair-severity {
+ font-size: 14px;
+ padding: 20px;
+
+ &__item {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 1px;
+ }
+
+ &__total {
+ font-weight: bold;
+ min-width: 48px;
+ padding-right: 8px;
+ text-align: end;
+ }
+
+ mat-icon {
+ font-size: 20px;
+ height: 20px;
+ margin-right: 8px;
+ width: 20px;
+ }
+
+ &__critical {
+ color: red;
+ }
+ &__high {
+ color: #ff7700
+ }
+ &__medium {
+ color: orange;
+ }
+ &__low {
+ color: #d29930;
+ }
+ &__negligible {
+ color: grey;
+ }
+ &__unknown {
+ color: #b8b8b8;
+ }
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.spec.ts
new file mode 100644
index 0000000000..1cabcdc5b6
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ClairReportSeveritySummaryComponent } from './clair-report-severity-summary.component';
+
+describe('ClairReportSeveritySummaryComponent', () => {
+ let component: ClairReportSeveritySummaryComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ClairReportSeveritySummaryComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ClairReportSeveritySummaryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.ts
new file mode 100644
index 0000000000..c945946e69
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component.ts
@@ -0,0 +1,30 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'app-clair-report-severity-summary',
+ templateUrl: './clair-report-severity-summary.component.html',
+ styleUrls: ['./clair-report-severity-summary.component.scss']
+})
+export class ClairReportSeveritySummaryComponent {
+
+ private severityTotals;
+
+ public severities: string[] = [];
+
+ public iconClassNames = {};
+
+ @Input()
+ set totals(totals) {
+ this.severityTotals = totals;
+ this.iconClassNames = {};
+ this.severities = Object.keys(totals);
+ this.severities.forEach(s => {
+ this.iconClassNames[s] = 'clair-severity__' + s.toLowerCase();
+ });
+ }
+
+ get totals() {
+ return this.severityTotals;
+ }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.html
new file mode 100644
index 0000000000..2becbd6645
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.html
@@ -0,0 +1,4 @@
+
+
warning
+
{{ severity }}
+
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.scss
new file mode 100644
index 0000000000..c8daa5a0cb
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.scss
@@ -0,0 +1,28 @@
+.clair-severity {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+
+ mat-icon {
+ margin-right: 8px;
+ }
+
+ &__critical {
+ color: red;
+ }
+ &__high {
+ color: #ff7700
+ }
+ &__medium {
+ color: orange;
+ }
+ &__low {
+ color: #d29930;
+ }
+ &__negligible {
+ color: grey;
+ }
+ &__unknown {
+ color: #b8b8b8;
+ }
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.spec.ts
new file mode 100644
index 0000000000..a0d20830b3
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ClairReportSeverityTableComponent } from './clair-report-severity-table.component';
+
+describe('ClairReportSeverityTableComponent', () => {
+ let component: ClairReportSeverityTableComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ClairReportSeverityTableComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ClairReportSeverityTableComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.ts
new file mode 100644
index 0000000000..38a0f592ff
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component.ts
@@ -0,0 +1,22 @@
+import { Component, OnInit } from '@angular/core';
+
+import { TableCellCustom } from '../../../../../shared/components/list/list.types';
+import { Vulnerability } from './../clair-report.types';
+
+@Component({
+ selector: 'app-clair-report-severity-table',
+ templateUrl: './clair-report-severity-table.component.html',
+ styleUrls: ['./clair-report-severity-table.component.scss']
+})
+export class ClairReportSeverityTableComponent extends TableCellCustom implements OnInit {
+
+ public severity: string;
+
+ public iconClasses: string[] = [];
+
+ ngOnInit() {
+ this.severity = this.row.Severity;
+ this.iconClasses = [ 'clair-severity__' + this.severity.toLowerCase() ];
+ }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.html
new file mode 100644
index 0000000000..89e13a8213
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
{{ image.image }}
+
+
{{ image.shortTag }}
+
{{ image.total }}
+
+
+
+
+
+
+
+
+
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.scss
new file mode 100644
index 0000000000..b0bb5f3a48
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.scss
@@ -0,0 +1,130 @@
+:host {
+ display: flex;
+ flex: 1;
+ height: calc(100vh - 104px);
+ margin: -20px;
+}
+
+.clair-report {
+
+ display: flex;
+ flex: 1;
+
+ &__imagelist {
+ border-right: 1px solid #ccc;
+ display: flex;
+ flex: 0 0 200px;
+ font-size: 14px;
+ flex-direction: column;
+ }
+
+ &__header {
+ align-items: center;
+ display: flex;
+ height: 40px;
+ font-size: 14px;
+ flex: 0 0 40px;
+ border-bottom: 1px solid #ccc;
+ background-color: #eee;
+ font-weight: bold;
+ opacity: 0.7;
+ padding: 0 10px;
+ }
+
+ &__header-label {
+ flex: 1;
+ }
+
+ &__header-count {
+ background-color: #ccc;
+ border-radius: 6px;
+ flex: 0;
+ font-size: 12px;
+ padding: 2px 10px;
+ }
+
+ &__imagelist-images {
+ flex: 1;
+ }
+
+ &__images {
+ overflow-y: auto;
+ }
+
+ overflow: hidden;
+ text-overflow: ellipsis;
+ /* line-height: 20px; */
+ /* word-break: break-all; */
+ white-space: nowrap;
+
+ &__detail {
+ flex: 1;
+ }
+
+ &__image {
+ border-left: 4px solid;
+ display: flex;
+ flex-direction: column;
+ cursor: pointer;
+ overflow: hidden;
+ width: 200px;
+ }
+
+ &__image-info {
+ display: flex;
+ flex-direction: column;
+ height: 40px;
+ justify-content: center;
+ padding: 0 10px;
+ }
+
+ &__image-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__image-subdetail {
+ display: flex;
+ }
+
+ &__image-tag {
+ flex: 1;
+ font-size: 12px;
+ opacity: 0.8;
+ }
+
+ &__image-total {
+ font-size: 12px;
+ opacity: 0.8;
+ text-align: end
+ }
+
+ &__active {
+ background-color: #eee;
+ //border-right: 4px solid #ccc;
+ }
+}
+
+
+.critical {
+ border-color: red;
+}
+.high {
+ border-color: #ff7700
+}
+.medium {
+ border-color: orange;
+}
+.low {
+ border-color: #d29930;
+}
+.negligable {
+ border-color: grey;
+}
+.unknown {
+ border-color: #b8b8b8;
+}
+.ok {
+ border-color: green;
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.spec.ts
new file mode 100644
index 0000000000..0d7adfa276
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ClairReportViewerComponent } from './clair-report-viewer.component';
+
+describe('ClairReportViewerComponent', () => {
+ let component: ClairReportViewerComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ClairReportViewerComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ClairReportViewerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.ts
new file mode 100644
index 0000000000..f82f0556a4
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report-viewer.component.ts
@@ -0,0 +1,143 @@
+import { Component, OnInit } from '@angular/core';
+
+import { IReportViewer } from '../analysis-report-viewer.component';
+import {
+ ClairSeverityOrder,
+} from './../../../../../../../../../custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report.types';
+
+interface ImageInfo {
+ name: string;
+ tag: string;
+ image: string;
+ registry: string;
+ info: ImageInfoDetail;
+ highestSeverity: string;
+ total: number;
+}
+
+interface ImageInfoDetail {
+ LayerCount: number;
+ Vulnerabilities: {string: number};
+}
+
+const ShortTagSize = 24;
+
+@Component({
+ selector: 'app-clair-report-viewer',
+ templateUrl: './clair-report-viewer.component.html',
+ styleUrls: ['./clair-report-viewer.component.scss']
+})
+export class ClairReportViewerComponent implements OnInit, IReportViewer {
+
+ report: any;
+
+ selected: string;
+
+ detail: any;
+
+ images: ImageInfo[];
+
+ constructor() { }
+
+ ngOnInit() {
+ this.images = [];
+ if (this.report && this.report.report && this.report.report.images) {
+ this.report.report.images.forEach(img => {
+ // Take the tag
+ const tagIndex = img.name.lastIndexOf(':');
+ const tag = img.name.substr(tagIndex + 1);
+ const name = img.name.substr(0, tagIndex);
+ const image = {
+ name: img.name,
+ tag,
+ shortTag: this.getShortTag(tag),
+ image: name,
+ registry: '',
+ info: img,
+ highestSeverity: this.getHighestSeverity(img),
+ total: this.getTotal(img)
+ };
+ this.parseImageName(image);
+ this.images.push(image);
+ });
+ }
+
+ // Sort the images based on severity
+ this.images = this.images.sort(this.sortBySeverityComparer);
+
+ // Auto-select first one
+ if (this.images.length > 0) {
+ this.selectImage(this.images[0].name);
+ }
+ }
+
+ private parseImageName(image: ImageInfo) {
+ const parts = image.image.split('/');
+
+ image.image = parts.pop();
+ image.registry = parts.join('/');
+ }
+
+ selectImage(name: string) {
+ this.selected = name;
+ const img = this.images.find(item => item.name === name);
+ if (img) {
+ this.detail = img.info;
+ }
+ }
+
+ // Compartor for sorting by severity
+ private sortBySeverityComparer(a: ImageInfo, b: ImageInfo): number {
+
+ // Go through the severities in order
+ for (const severity of ClairSeverityOrder) {
+ const sa = getSeverity(a, severity);
+ const sb = getSeverity(b, severity);
+ if (sa !== sb) {
+ return sb - sa;
+ }
+ }
+ // Must be the same
+ return 0;
+ }
+
+ private getShortTag(tag: string): string {
+
+ console.log(tag.length);
+ console.log(tag);
+
+ const i =tag.length <= ShortTagSize ? tag : tag.substr(0, ShortTagSize) + '...';
+ console.log(i);
+ return i;
+
+ }
+
+ private getTotal(info: ImageInfoDetail): number {
+ let total = 0;
+ for (const severity of ClairSeverityOrder) {
+ const count = info.Vulnerabilities[severity] ? info.Vulnerabilities[severity] : 0;
+ total += count;
+ }
+
+ return total;
+ }
+
+ private getHighestSeverity(info: ImageInfoDetail): string {
+ for (const severity of ClairSeverityOrder) {
+ const count = info.Vulnerabilities[severity] ? info.Vulnerabilities[severity] : 0;
+ if (count > 0) {
+ return severity.toLowerCase();
+ }
+ }
+
+ // There are no vulnerabilities
+ return 'ok';
+ }
+}
+
+function getSeverity(img: ImageInfo, severity: string): number {
+ if (!img.info || !img.info.Vulnerabilities) {
+ return 0;
+ }
+ return img.info.Vulnerabilities[severity] ? img.info.Vulnerabilities[severity] : 0;
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report.types.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report.types.ts
new file mode 100644
index 0000000000..e951bcca65
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/clair-report-viewer/clair-report.types.ts
@@ -0,0 +1,27 @@
+export const ClairSeverityOrder = [ 'Critical', 'High', 'Medium', 'Low', 'Negligible', 'Unknown' ];
+
+export interface KlarReport {
+ LayerCount: number;
+ Vulnerabiltiies: Vulnerabilities;
+}
+
+export interface Vulnerabilities {
+ Critiical: Vulnerability[];
+ High: Vulnerability[];
+ Low: Vulnerability[];
+ Medium: Vulnerability[];
+ Negligable: Vulnerability[];
+ Unknown: Vulnerability[];
+}
+
+export interface Vulnerability {
+ Description: string;
+ FeatureName: string;
+ FeatureVersion: string;
+ FixedBy: string;
+ Link: string;
+ Metadata?: any;
+ Name: string;
+ NamespaceName: string;
+ Severity: string;
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.html
new file mode 100644
index 0000000000..1ef1086e47
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.html
@@ -0,0 +1 @@
+json-report-viewer works!
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.spec.ts
new file mode 100644
index 0000000000..67b2c9ad92
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.spec.ts
@@ -0,0 +1,38 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { MDAppModule } from './../../../../core/md.module';
+
+import { JsonReportViewerComponent } from './json-report-viewer.component';
+import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../kubernetes.testing.module';
+import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service';
+
+describe('JsonReportViewerComponent', () => {
+ let component: JsonReportViewerComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ JsonReportViewerComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+ MDAppModule
+ ],
+ providers: [
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(JsonReportViewerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.ts
new file mode 100644
index 0000000000..7157b00f0c
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/json-report-viewer/json-report-viewer.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-json-report-viewer',
+ templateUrl: './json-report-viewer.component.html',
+ styleUrls: ['./json-report-viewer.component.scss']
+})
+export class JsonReportViewerComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit() {
+ }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.html
new file mode 100644
index 0000000000..94727c16ab
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.html
@@ -0,0 +1 @@
+junit-report-viewer works!
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.spec.ts
new file mode 100644
index 0000000000..53fa90e44f
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { JUnitReportViewerComponent } from './junit-report-viewer.component';
+
+describe('JUnitReportViewerComponent', () => {
+ let component: JUnitReportViewerComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ JUnitReportViewerComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(JUnitReportViewerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.ts
new file mode 100644
index 0000000000..9c27302eb2
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/junit-report-viewer/junit-report-viewer.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-junit-report-viewer',
+ templateUrl: './junit-report-viewer.component.html',
+ styleUrls: ['./junit-report-viewer.component.scss']
+})
+export class JUnitReportViewerComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit() {
+ }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.html
new file mode 100644
index 0000000000..a5dfac6b45
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.html
@@ -0,0 +1,18 @@
+
+
{{ group._name }}
+
+
+
+ check_circle
+ info
+ warning
+ error
+ help_outline
+
+
{{ check.Check.Name }}
+
+
+
+
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.scss
new file mode 100644
index 0000000000..6c76b78b25
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.scss
@@ -0,0 +1,23 @@
+.report {
+ &__group {
+ margin-bottom: 10px;
+ }
+ &__group-name {
+ font-weight: bold;
+ padding: 4px 0;
+ }
+ &__check {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ margin-left: 10px;
+ }
+ &__icon {
+ margin-right: 4px;
+ }
+ &__comment {
+ display: list-item;
+ list-style: square;
+ margin-left: 58px;
+ }
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.spec.ts
new file mode 100644
index 0000000000..a93f22f618
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.spec.ts
@@ -0,0 +1,38 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { MDAppModule } from './../../../../core/md.module';
+
+import { KubeScoreReportViewerComponent } from './kube-score-report-viewer.component';
+import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../kubernetes.testing.module';
+import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service';
+
+describe('KubeScoreReportViewerComponent', () => {
+ let component: KubeScoreReportViewerComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ KubeScoreReportViewerComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+ MDAppModule
+ ],
+ providers: [
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(KubeScoreReportViewerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.ts
new file mode 100644
index 0000000000..11601dc155
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.ts
@@ -0,0 +1,50 @@
+import { Component, OnInit } from '@angular/core';
+import { IReportViewer } from '../analysis-report-viewer.component';
+
+@Component({
+ selector: 'app-kube-score-report-viewer',
+ templateUrl: './kube-score-report-viewer.component.html',
+ styleUrls: ['./kube-score-report-viewer.component.scss']
+})
+export class KubeScoreReportViewerComponent implements OnInit, IReportViewer {
+
+ // Kube Score grading
+ /* See: https://github.com/zegl/kube-score/blob/eca7bda47f5b3c523a0f41945cb1adda0a4e2e2e/scorecard/scorecard.go
+ // GradeCritical Grade = 1
+ // GradeWarning Grade = 5
+ // GradeAlmostOK Grade = 7
+ // GradeAllOK Grade = 10
+ */
+
+ report: any;
+ processed: any;
+
+ constructor() { }
+
+ ngOnInit() {
+ this.processed = [];
+ // Turn the report into an array
+ if (this.report) {
+ Object.keys(this.report.report).forEach(key => {
+ const filtered = this.filter(this.report.report[key]);
+ if (filtered.length > 0) {
+ this.processed.push({
+ ...this.report.report[key],
+ _checks: filtered,
+ _name: key,
+ });
+ }
+ });
+ }
+ }
+
+ public filter(report) {
+ const filtered = [];
+ report.Checks.forEach(r => {
+ if (r.Grade !== 10 && !r.Skipped) {
+ filtered.push(r);
+ }
+ });
+ return filtered;
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.html
new file mode 100644
index 0000000000..6b198581cf
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+ {{ group.name }} |
+
+
+
+ check_circle
+ info
+ warning
+ error
+ help_outline
+
+ {{ issue.message }}
+
+ |
+
+
+
+
+
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.scss
new file mode 100644
index 0000000000..f70ee0c2c3
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.scss
@@ -0,0 +1,43 @@
+.report {
+ &__report-header {
+ align-items: center;
+ display: flex;
+ margin-bottom: 8px;
+ }
+ &__header {
+ align-items: center;
+ display: flex;
+ }
+ &__title {
+ flex: 1;
+ }
+ &__stat {
+ display: flex;
+ flex-direction: column;
+ padding: 5px 12px;
+ &>div:first-child {
+ opacity: 0.8;
+ }
+ }
+ &__score {
+ flex: 0;
+ font-size: 20px;
+ }
+ &__grade {
+ flex: 0;
+ font-size: 20px;
+ }
+ &__table {
+ margin-left: 20px;
+ }
+ &__issue {
+ align-items: center;
+ display: flex;
+ }
+ &__icon {
+ padding-right: 4px;
+ }
+ &__table-name {
+ vertical-align: top;
+ }
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.spec.ts
new file mode 100644
index 0000000000..ccbe5c5081
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.spec.ts
@@ -0,0 +1,38 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { MDAppModule } from './../../../../core/md.module';
+
+import { PopeyeReportViewerComponent } from './popeye-report-viewer.component';
+import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../kubernetes.testing.module';
+import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service';
+
+describe('PopeyeReportViewerComponent', () => {
+ let component: PopeyeReportViewerComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ PopeyeReportViewerComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+ MDAppModule
+ ],
+ providers: [
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PopeyeReportViewerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.ts
new file mode 100644
index 0000000000..13feda8759
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.ts
@@ -0,0 +1,47 @@
+import { Component, OnInit } from '@angular/core';
+import { IReportViewer } from '../analysis-report-viewer.component';
+
+@Component({
+ selector: 'app-popeye-report-viewer',
+ templateUrl: './popeye-report-viewer.component.html',
+ styleUrls: ['./popeye-report-viewer.component.scss']
+})
+export class PopeyeReportViewerComponent implements OnInit, IReportViewer {
+
+ report: any;
+ processed: any;
+
+ constructor() { }
+
+ ngOnInit() {
+ this.processed = this.apply(this.report);
+ }
+
+ private apply(response) {
+ if (response) {
+ // Make the response easier to render
+ response.report.popeye.sanitizers.forEach(s => {
+ const groups = [];
+ let totalIssues = 0;
+ if (s.issues) {
+ Object.keys(s.issues).forEach(key => {
+ const issues = s.issues[key];
+ totalIssues += issues.length;
+ if (issues.length > 0) {
+ groups.push({
+ name: key,
+ issues
+ });
+ }
+ });
+ s.hide = totalIssues === 0;
+ } else {
+ s.hide = true;
+ }
+ s.groups = groups;
+ });
+
+ return response.report;
+ }
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.html
new file mode 100644
index 0000000000..0e7a5b9605
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.html
@@ -0,0 +1,5 @@
+
+
+
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.scss
new file mode 100644
index 0000000000..d7d483462b
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.scss
@@ -0,0 +1,14 @@
+.alert {
+ &__info {
+ align-items: center;
+ display: flex;
+ margin: 4px 20px;
+ }
+ &__icon {
+ margin-right: 8px;
+ }
+ &__group {
+ font-weight: bold;
+ padding: 4px 0;
+ }
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.spec.ts
new file mode 100644
index 0000000000..f78f26e839
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.spec.ts
@@ -0,0 +1,34 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ResourceAlertPreviewComponent } from './resource-alert-preview.component';
+import { ResourceAlertViewComponent } from './resource-alert-view/resource-alert-view.component';
+import { SidePanelService } from 'frontend/packages/core/src/shared/services/side-panel.service';
+import { KubernetesBaseTestModules } from '../../kubernetes.testing.module';
+
+describe('ResourceAlertPreviewComponent', () => {
+ let component: ResourceAlertPreviewComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ResourceAlertPreviewComponent, ResourceAlertViewComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+ ],
+ providers: [
+ SidePanelService,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ResourceAlertPreviewComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.ts
new file mode 100644
index 0000000000..7b24b6b212
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.ts
@@ -0,0 +1,24 @@
+import { Component } from '@angular/core';
+
+import { PreviewableComponent } from './../../../../shared/previewable-component';
+
+@Component({
+ selector: 'app-resource-alert-preview',
+ templateUrl: './resource-alert-preview.component.html',
+ styleUrls: ['./resource-alert-preview.component.scss']
+})
+export class ResourceAlertPreviewComponent implements PreviewableComponent {
+
+ title: string;
+
+ resource: any;
+ alerts: any;
+
+ constructor() { }
+
+ setProps(props: { [key: string]: any; }): void {
+ this.resource = props.resource;
+ this.title = `${this.resource.kind} Alerts`;
+ }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.html b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.html
new file mode 100644
index 0000000000..f9193d84c4
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.html
@@ -0,0 +1,16 @@
+
+
+
{{group.name}}
+
+
+
+ info
+ warning
+ error
+ help_outline
+
+ {{ alert.message }}
+
+
+
+
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.scss b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.scss
new file mode 100644
index 0000000000..d7d483462b
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.scss
@@ -0,0 +1,14 @@
+.alert {
+ &__info {
+ align-items: center;
+ display: flex;
+ margin: 4px 20px;
+ }
+ &__icon {
+ margin-right: 8px;
+ }
+ &__group {
+ font-weight: bold;
+ padding: 4px 0;
+ }
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.spec.ts
new file mode 100644
index 0000000000..0ffbf5f5ff
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.spec.ts
@@ -0,0 +1,38 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { MDAppModule } from './../../../../../core/md.module';
+
+import { ResourceAlertViewComponent } from './resource-alert-view.component';
+import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../../kubernetes.testing.module';
+import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../../services/kubernetes-endpoint.service';
+
+describe('ResourceAlertViewComponent', () => {
+ let component: ResourceAlertViewComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ResourceAlertViewComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+ MDAppModule
+ ],
+ providers: [
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ResourceAlertViewComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.ts b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.ts
new file mode 100644
index 0000000000..ccd0cb203f
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.ts
@@ -0,0 +1,48 @@
+import { Component, OnInit, Input } from '@angular/core';
+
+@Component({
+ selector: 'app-resource-alert-view',
+ templateUrl: './resource-alert-view.component.html',
+ styleUrls: ['./resource-alert-view.component.scss']
+})
+export class ResourceAlertViewComponent implements OnInit {
+
+ alertInfo;
+
+ @Input()
+ set alerts(data: any) {
+ if (data) {
+ const alerts = data.alerts ? data.alerts : data;
+ this.alertInfo = this.normalize(alerts);
+ }
+ }
+
+ @Input() showHeader = true;
+
+ constructor() { }
+
+ ngOnInit() { }
+
+ normalize(data) {
+ // Normalize the alerts into groups
+ const normalized = {};
+ data.forEach(item => {
+ const path = item.namespace ? `${item.namespace}/${item.name}` : item.name;
+ if (!normalized[path]) {
+ normalized[path] = [];
+ }
+ item.path = path;
+ normalized[path].push(item);
+ });
+
+ const arr = [];
+ Object.keys(normalized).forEach(group => {
+ arr.push({
+ name: group,
+ alerts: normalized[group]
+ });
+ });
+ return arr;
+ }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-factory.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-factory.ts
index 787e1a6024..46b7045a23 100644
--- a/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-factory.ts
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-factory.ts
@@ -15,6 +15,7 @@ export const kubernetesStatefulSetsEntityType = 'kubernetesStatefulSet';
export const kubernetesDeploymentsEntityType = 'kubernetesDeployment';
export const kubernetesAppsEntityType = 'kubernetesApp';
export const kubernetesDashboardEntityType = 'kubernetesDashboard';
+export const analysisReportEntityType = 'analysisReport';
export const getKubeAppId = (object: KubernetesApp) => object.name;
@@ -97,6 +98,13 @@ entityCache[kubernetesDashboardEntityType] = new KubernetesEntitySchema(
{ idAttribute: getKubeAPIResourceGuid }
);
+// Analysis Reports - should not be bound to an endpoint
+entityCache[analysisReportEntityType] = new KubernetesEntitySchema(
+ analysisReportEntityType,
+ {},
+ { idAttribute: (entity) => entity.id }
+);
+
entityCache[metricEntityType] = new KubernetesEntitySchema(metricEntityType);
export function addKubernetesEntitySchema(key: string, newSchema: EntitySchema) {
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts
index cd4b43f25a..1e8f43482d 100644
--- a/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts
@@ -28,6 +28,7 @@ import {
kubernetesPodsEntityType,
kubernetesServicesEntityType,
kubernetesStatefulSetsEntityType,
+ analysisReportEntityType,
} from './kubernetes-entity-factory';
import {
KubernetesApp,
@@ -147,6 +148,7 @@ export function generateKubernetesEntities(): StratosBaseCatalogEntity[] {
generateNamespacesEntity(endpointDefinition),
generateServicesEntity(endpointDefinition),
generateDashboardEntity(endpointDefinition),
+ generateAnalysisReportsEntity(endpointDefinition),
generateMetricEntity(endpointDefinition),
...generateWorkloadsEntities(endpointDefinition)
];
@@ -231,6 +233,15 @@ function generateDashboardEntity(endpointDefinition: StratosEndpointExtensionDef
return new StratosCatalogEntity(definition);
}
+function generateAnalysisReportsEntity(endpointDefinition: StratosEndpointExtensionDefinition) {
+ const definition = {
+ type: analysisReportEntityType,
+ schema: kubernetesEntityFactory(analysisReportEntityType),
+ endpoint: endpointDefinition
+ };
+ return new StratosCatalogEntity(definition);
+}
+
function generateMetricEntity(endpointDefinition: StratosEndpointExtensionDefinition) {
const definition = {
type: metricEntityType,
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.html b/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.html
new file mode 100644
index 0000000000..09f0aa7b2a
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.scss b/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.spec.ts
new file mode 100644
index 0000000000..5e5fdfaf98
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.spec.ts
@@ -0,0 +1,46 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { MDAppModule } from './../../../../core/md.module';
+
+import { KubernetesNamespaceAnalysisReportComponent } from './kubernetes-namespace-analysis-report.component';
+import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../kubernetes.testing.module';
+import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service';
+import {
+ AnalysisReportSelectorComponent
+} from './../../analysis-report-viewer/analysis-report-selector/analysis-report-selector.component';
+import { AnalysisReportViewerComponent } from './../../analysis-report-viewer/analysis-report-viewer.component';
+import { KubernetesNamespaceService } from '../../services/kubernetes-namespace.service';
+import { TabNavService } from 'frontend/packages/core/tab-nav.service';
+
+describe('KubernetesNamespaceAnalysisReportComponent', () => {
+ let component: KubernetesNamespaceAnalysisReportComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ KubernetesNamespaceAnalysisReportComponent, AnalysisReportSelectorComponent, AnalysisReportViewerComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+ MDAppModule
+ ],
+ providers: [
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
+ KubernetesNamespaceService,
+ TabNavService,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(KubernetesNamespaceAnalysisReportComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.ts
new file mode 100644
index 0000000000..ed40078630
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.ts
@@ -0,0 +1,52 @@
+import { Component } from '@angular/core';
+import { Subject } from 'rxjs';
+import { AnalysisReport } from '../../store/kube.types';
+import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service';
+import { KubernetesNamespaceService } from '../../services/kubernetes-namespace.service';
+
+@Component({
+ selector: 'app-kubernetes-namespace-analysis-report-tab',
+ templateUrl: './kubernetes-namespace-analysis-report.component.html',
+ styleUrls: ['./kubernetes-namespace-analysis-report.component.scss'],
+ providers: [
+ KubernetesAnalysisService
+ ]
+})
+export class KubernetesNamespaceAnalysisReportComponent {
+
+ public report$ = new Subject();
+
+ path: string;
+
+ currentReport = null;
+
+ endpointID: string;
+
+ noReportsAvailable = false;
+
+ constructor(
+ public analyzerService: KubernetesAnalysisService,
+ public endpointService: KubernetesEndpointService,
+ public kubeNamespaceService: KubernetesNamespaceService,
+ ) {
+ this.endpointID = this.endpointService.kubeGuid;
+ this.path = `${this.kubeNamespaceService.namespaceName}`;
+ this.report$.next(null);
+ }
+
+ public analysisChanged(report) {
+ if (report.id !== this.currentReport) {
+ this.currentReport = report.id;
+ this.analyzerService.getByID(report.id).subscribe(r => this.report$.next(r));
+ }
+ }
+
+ public onReportCount(count: number) {
+ this.noReportsAvailable = count === 0;
+ }
+
+ public runAnalysis(id: string) {
+ this.analyzerService.run(id, this.endpointID, this.kubeNamespaceService.namespaceName);
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts
index ebe43a1a09..2dfbbfe239 100644
--- a/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts
@@ -8,6 +8,7 @@ import { BaseKubeGuid } from '../kubernetes-page.types';
import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service';
import { KubernetesNamespaceService } from '../services/kubernetes-namespace.service';
import { KubernetesService } from '../services/kubernetes.service';
+import { KubernetesAnalysisService } from '../services/kubernetes.analysis.service';
@Component({
selector: 'app-kubernetes-namespace',
@@ -27,21 +28,20 @@ import { KubernetesService } from '../services/kubernetes.service';
},
KubernetesService,
KubernetesEndpointService,
- KubernetesNamespaceService
+ KubernetesNamespaceService,
+ KubernetesAnalysisService,
]
})
export class KubernetesNamespaceComponent {
- tabLinks = [
- { link: 'pods', label: 'Pods', icon: 'adjust' },
- { link: 'services', label: 'Services', icon: 'service', iconFont: 'stratos-icons' }
- ];
+ tabLinks = [];
public breadcrumbs$: Observable;
constructor(
public kubeEndpointService: KubernetesEndpointService,
- public kubeNamespaceService: KubernetesNamespaceService
+ public kubeNamespaceService: KubernetesNamespaceService,
+ public analysisService: KubernetesAnalysisService,
) {
this.breadcrumbs$ = kubeEndpointService.endpoint$.pipe(
map(endpoint => ([{
@@ -51,5 +51,11 @@ export class KubernetesNamespaceComponent {
}])
)
);
+
+ this.tabLinks = [
+ { link: 'pods', label: 'Pods', icon: 'adjust' },
+ { link: 'services', label: 'Services', icon: 'service', iconFont: 'stratos-icons' },
+ { link: 'analysis', label: 'Analysis', icon: 'assignment', hidden$: this.analysisService.hideAnalysis$ },
+ ];
}
}
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.html b/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.html
index cb780e3041..eadc2287b1 100644
--- a/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.html
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.html
@@ -13,7 +13,7 @@
{{ resource.age }}
-
+
{{ label.name }}
{{ label.value }}
@@ -27,6 +27,10 @@
+
+
+
+
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.spec.ts
index 5df03aea30..75fb79c994 100644
--- a/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.spec.ts
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.spec.ts
@@ -5,6 +5,7 @@ import { SidePanelService } from '../../../shared/services/side-panel.service';
import { KubeBaseGuidMock, KubernetesBaseTestModules } from '../kubernetes.testing.module';
import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service';
import { KubernetesResourceViewerComponent } from './kubernetes-resource-viewer.component';
+import { ResourceAlertViewComponent } from './../analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component';
describe('KubernetesResourceViewerComponent', () => {
let component: KubernetesResourceViewerComponent;
@@ -12,7 +13,7 @@ describe('KubernetesResourceViewerComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
- declarations: [KubernetesResourceViewerComponent],
+ declarations: [KubernetesResourceViewerComponent, KubernetesResourceViewerComponent, ResourceAlertViewComponent],
imports: KubernetesBaseTestModules,
providers: [
KubernetesEndpointService,
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.ts
index 1fbe76d824..9c0283eab9 100644
--- a/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.ts
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.ts
@@ -10,6 +10,7 @@ import { BasicKubeAPIResource, KubeAPIResource } from '../store/kube.types';
export interface KubernetesResourceViewerConfig {
title: string;
+ analysis?: any;
resource$: Observable;
resourceKind: string;
}
@@ -44,32 +45,41 @@ export class KubernetesResourceViewerComponent implements PreviewableComponent {
public hasPodMetrics$: Observable;
public podRouterLink$: Observable;
+ private analysis;
+ private alerts;
+
setProps(props: KubernetesResourceViewerConfig) {
this.title = props.title;
+ this.analysis = props.analysis;
this.resource$ = props.resource$.pipe(
map((item: any) => {// KubeAPIResource
const resource: KubernetesResourceViewerResource = {} as KubernetesResourceViewerResource;
const newItem = {} as any;
resource.raw = item;
-
Object.keys(item || []).forEach(k => {
- if (k !== 'endpointId' && k !== 'releaseTitle' && k !== 'expandedStatus') {
+ if (k !== 'endpointId' && k !== 'releaseTitle' && k !== 'expandedStatus' && k !== '_metadata' ) {
newItem[k] = item[k];
}
});
resource.jsonView = newItem;
- resource.age = moment(item.metadata.creationTimestamp).fromNow(true);
- resource.creationTimestamp = item.metadata.creationTimestamp;
-
- resource.labels = [];
- Object.keys(item.metadata.labels || []).forEach(labelName => {
- resource.labels.push({
- name: labelName,
- value: item.metadata.labels[labelName]
+
+ const fallback = item._metadata ? item._metadata : {};
+
+ const ts = item.metadata ? item.metadata.creationTimestamp : fallback.creationTimestamp;
+ resource.age = moment(ts).fromNow(true);
+ resource.creationTimestamp = ts;
+
+ if (item.metadata && item.metadata.labels) {
+ resource.labels = [];
+ Object.keys(item.metadata.labels || []).forEach(labelName => {
+ resource.labels.push({
+ name: labelName,
+ value: item.metadata.labels[labelName]
+ });
});
- });
+ }
if (item.metadata && item.metadata.annotations) {
resource.annotations = [];
@@ -81,8 +91,13 @@ export class KubernetesResourceViewerComponent implements PreviewableComponent {
});
}
- resource.kind = item.kind || props.resourceKind;
- resource.apiVersion = item.apiVersion || this.getVersionFromSelfLink(item.metadata.selfLink);
+ resource.kind = item.kind || fallback.kind || props.resourceKind;
+ resource.apiVersion = item.apiVersion || fallback.apiVersion || this.getVersionFromSelfLink(item.metadata.selfLink);
+
+ // Apply analysis if there is one - if this is a k8s resource (i.e. not a container)
+ if (item.metadata) {
+ this.applyAnalysis(resource);
+ }
return resource;
}),
publishReplay(1),
@@ -123,4 +138,14 @@ export class KubernetesResourceViewerComponent implements PreviewableComponent {
return this.kubeEndpointService.kubeGuid || res.endpointId || res.metadata.kubeId;
}
+ private applyAnalysis(resource) {
+ let id = (resource.kind || 'pod').toLowerCase();
+ id = `${id}/${resource.raw.metadata.namespace}/${resource.raw.metadata.name}`;
+ if (this.analysis && this.analysis.alerts[id]) {
+ this.alerts = this.analysis.alerts[id];
+ } else {
+ this.alerts = null;
+ }
+ }
+
}
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-tab-base/kubernetes-tab-base.component.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes-tab-base/kubernetes-tab-base.component.ts
index b61ebc2c76..43f276712c 100644
--- a/custom-src/frontend/app/custom/kubernetes/kubernetes-tab-base/kubernetes-tab-base.component.ts
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-tab-base/kubernetes-tab-base.component.ts
@@ -1,13 +1,14 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
-import { first, map, startWith } from 'rxjs/operators';
+import { first, map, startWith, tap } from 'rxjs/operators';
import { UserFavoriteEndpoint } from '../../../../../store/src/types/user-favorites.types';
import { FavoritesConfigMapper } from '../../../shared/components/favorites-meta-card/favorite-config-mapper';
import { BaseKubeGuid } from '../kubernetes-page.types';
import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service';
import { KubernetesService } from '../services/kubernetes.service';
+import { KubernetesAnalysisService } from '../services/kubernetes.analysis.service';
@Component({
selector: 'app-kubernetes-tab-base',
@@ -27,16 +28,12 @@ import { KubernetesService } from '../services/kubernetes.service';
},
KubernetesService,
KubernetesEndpointService,
+ KubernetesAnalysisService,
]
})
export class KubernetesTabBaseComponent implements OnInit {
- tabLinks = [
- { link: 'summary', label: 'Summary', icon: 'kubernetes', iconFont: 'stratos-icons' },
- { link: 'nodes', label: 'Nodes', icon: 'developer_board' },
- { link: 'namespaces', label: 'Namespaces', icon: 'language' },
- { link: 'pods', label: 'Pods', icon: 'adjust' },
- ];
+ tabLinks = [];
public isFetching$: Observable;
public favorite$: Observable;
@@ -44,7 +41,19 @@ export class KubernetesTabBaseComponent implements OnInit {
constructor(
public kubeEndpointService: KubernetesEndpointService,
- public favoritesConfigMapper: FavoritesConfigMapper) { }
+ public favoritesConfigMapper: FavoritesConfigMapper,
+ public analysisService: KubernetesAnalysisService,
+ ) {
+ this.tabLinks = [
+ { link: 'summary', label: 'Summary', icon: 'kubernetes', iconFont: 'stratos-icons' },
+ { link: 'analysis', label: 'Analysis', icon: 'assignment', hidden$: this.analysisService.hideAnalysis$ },
+ { link: '-', label: 'Cluster' },
+ { link: 'nodes', label: 'Nodes', icon: 'developer_board' },
+ { link: 'namespaces', label: 'Namespaces', icon: 'language' },
+ { link: '-', label: 'Resources' },
+ { link: 'pods', label: 'Pods', icon: 'adjust' },
+ ];
+ }
ngOnInit() {
this.isFetching$ = this.kubeEndpointService.endpoint$.pipe(
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts
index ed05e9af79..bfe0d6b980 100644
--- a/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts
@@ -1,14 +1,42 @@
-/* tslint:disable:max-line-length */
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { NgxChartsModule } from '@swimlane/ngx-charts';
import { CoreModule } from '../../core/core.module';
import { SharedModule } from '../../shared/shared.module';
+import {
+ AnalysisReportSelectorComponent,
+} from './analysis-report-viewer/analysis-report-selector/analysis-report-selector.component';
+import { AnalysisReportViewerComponent } from './analysis-report-viewer/analysis-report-viewer.component';
+import {
+ ClairReportDetailComponent,
+} from './analysis-report-viewer/clair-report-viewer/clair-report-detail/clair-report-detail.component';
+import {
+ ClairReportSeveritySummaryComponent,
+} from './analysis-report-viewer/clair-report-viewer/clair-report-severity-summary/clair-report-severity-summary.component';
+import {
+ ClairReportSeverityTableComponent,
+} from './analysis-report-viewer/clair-report-viewer/clair-report-severity-table/clair-report-severity-table.component';
+import { ClairReportViewerComponent } from './analysis-report-viewer/clair-report-viewer/clair-report-viewer.component';
+import { JsonReportViewerComponent } from './analysis-report-viewer/json-report-viewer/json-report-viewer.component';
+import { JUnitReportViewerComponent } from './analysis-report-viewer/junit-report-viewer/junit-report-viewer.component';
+import {
+ KubeScoreReportViewerComponent,
+} from './analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component';
+import { PopeyeReportViewerComponent } from './analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component';
+import {
+ ResourceAlertPreviewComponent,
+} from './analysis-report-viewer/resource-alert-preview/resource-alert-preview.component';
+import {
+ ResourceAlertViewComponent,
+} from './analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component';
import {
KubedashConfigurationComponent,
} from './kubernetes-dashboard/kubedash-configuration/kubedash-configuration.component';
import { KubernetesDashboardTabComponent } from './kubernetes-dashboard/kubernetes-dashboard.component';
+import {
+ KubernetesNamespaceAnalysisReportComponent,
+} from './kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component';
import {
KubernetesNamespacePodsComponent,
} from './kubernetes-namespace/kubernetes-namespace-pods/kubernetes-namespace-pods.component';
@@ -33,6 +61,7 @@ import { KubernetesResourceViewerComponent } from './kubernetes-resource-viewer/
import { KubernetesTabBaseComponent } from './kubernetes-tab-base/kubernetes-tab-base.component';
import { KubernetesRoutingModule } from './kubernetes.routing';
import { KubernetesComponent } from './kubernetes/kubernetes.component';
+import { AnalysisStatusCellComponent } from './list-types/analysis-status-cell/analysis-status-cell.component';
import { KubernetesLabelsCellComponent } from './list-types/kubernetes-labels-cell/kubernetes-labels-cell.component';
import {
KubeNamespacePodCountComponent,
@@ -88,12 +117,22 @@ import { PodMetricsComponent } from './pod-metrics/pod-metrics.component';
import { KubernetesEndpointService } from './services/kubernetes-endpoint.service';
import { KubernetesNodeService } from './services/kubernetes-node.service';
import { KubernetesService } from './services/kubernetes.service';
+import {
+ AnalysisInfoCardComponent,
+} from './tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component';
+import {
+ KubernetesAnalysisInfoComponent,
+} from './tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component';
+import {
+ KubernetesAnalysisReportComponent,
+} from './tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component';
+import { KubernetesAnalysisTabComponent } from './tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component';
import { KubernetesNamespacesTabComponent } from './tabs/kubernetes-namespaces-tab/kubernetes-namespaces-tab.component';
import { KubernetesNodesTabComponent } from './tabs/kubernetes-nodes-tab/kubernetes-nodes-tab.component';
import { KubernetesPodsTabComponent } from './tabs/kubernetes-pods-tab/kubernetes-pods-tab.component';
import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kubernetes-summary.component';
-
+/* tslint:disable:max-line-length */
/* tslint:enable */
@NgModule({
@@ -114,6 +153,7 @@ import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kub
KubernetesNamespacesTabComponent,
KubernetesDashboardTabComponent,
KubernetesSummaryTabComponent,
+ KubernetesAnalysisTabComponent,
PodMetricsComponent,
KubernetesNodeLinkComponent,
KubernetesNodeIpsComponent,
@@ -146,7 +186,24 @@ import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kub
KubernetesResourceViewerComponent,
KubeServiceCardComponent,
KubedashConfigurationComponent,
- KubernetesPodContainersComponent
+ KubernetesPodContainersComponent,
+ KubernetesAnalysisReportComponent,
+ KubernetesAnalysisInfoComponent,
+ AnalysisInfoCardComponent,
+ AnalysisReportViewerComponent,
+ PopeyeReportViewerComponent,
+ JUnitReportViewerComponent,
+ JsonReportViewerComponent,
+ ClairReportViewerComponent,
+ ClairReportDetailComponent,
+ AnalysisReportSelectorComponent,
+ ResourceAlertPreviewComponent,
+ ResourceAlertViewComponent,
+ KubeScoreReportViewerComponent,
+ AnalysisStatusCellComponent,
+ KubernetesNamespaceAnalysisReportComponent,
+ ClairReportSeveritySummaryComponent,
+ ClairReportSeverityTableComponent,
],
providers: [
KubernetesService,
@@ -170,10 +227,34 @@ import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kub
KubernetesPodStatusComponent,
KubeServiceCardComponent,
KubernetesResourceViewerComponent,
- KubernetesPodContainersComponent
+ KubernetesPodContainersComponent,
+ PopeyeReportViewerComponent,
+ JUnitReportViewerComponent,
+ JsonReportViewerComponent,
+ ClairReportViewerComponent,
+ ClairReportDetailComponent,
+ KubeScoreReportViewerComponent,
+ AnalysisReportSelectorComponent,
+ ResourceAlertPreviewComponent,
+ AnalysisStatusCellComponent,
+ ClairReportSeveritySummaryComponent,
+ ClairReportSeverityTableComponent,
],
exports: [
- KubernetesResourceViewerComponent
+ KubernetesResourceViewerComponent,
+ AnalysisReportViewerComponent,
+ PopeyeReportViewerComponent,
+ JUnitReportViewerComponent,
+ JsonReportViewerComponent,
+ ClairReportViewerComponent,
+ ClairReportDetailComponent,
+ KubeScoreReportViewerComponent,
+ AnalysisReportSelectorComponent,
+ ResourceAlertPreviewComponent,
+ ResourceAlertViewComponent,
+ AnalysisStatusCellComponent,
+ ClairReportSeveritySummaryComponent,
+ ClairReportSeverityTableComponent,
]
})
export class KubernetesModule { }
diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes.routing.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes.routing.ts
index da6956e8a5..1cbcadd40b 100644
--- a/custom-src/frontend/app/custom/kubernetes/kubernetes.routing.ts
+++ b/custom-src/frontend/app/custom/kubernetes/kubernetes.routing.ts
@@ -1,3 +1,4 @@
+import { KubernetesNamespaceAnalysisReportComponent } from './kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@@ -23,6 +24,11 @@ import { KubernetesNodesTabComponent } from './tabs/kubernetes-nodes-tab/kuberne
import { KubernetesPodsTabComponent } from './tabs/kubernetes-pods-tab/kubernetes-pods-tab.component';
import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kubernetes-summary.component';
import { KubedashConfigurationComponent } from './kubernetes-dashboard/kubedash-configuration/kubedash-configuration.component';
+import { KubernetesAnalysisTabComponent } from './tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component';
+import { KubernetesAnalysisReportComponent } from './tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component';
+import {
+ KubernetesAnalysisInfoComponent
+} from './tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component';
const kubernetes: Routes = [{
path: '',
@@ -80,7 +86,10 @@ const kubernetes: Routes = [{
path: 'services',
component: KubernetesNamespaceServicesComponent
},
-
+ {
+ path: 'analysis',
+ component: KubernetesNamespaceAnalysisReportComponent
+ }
]
},
{
@@ -108,6 +117,18 @@ const kubernetes: Routes = [{
path: 'pods',
component: KubernetesPodsTabComponent
},
+ {
+ path: 'analysis',
+ component: KubernetesAnalysisTabComponent
+ },
+ {
+ path: 'analysis/report/:id',
+ component: KubernetesAnalysisReportComponent
+ },
+ {
+ path: 'analysis/info',
+ component: KubernetesAnalysisInfoComponent
+ },
]
},
{
diff --git a/custom-src/frontend/app/custom/kubernetes/list-types/analysis-reports-list-config.service.ts b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-reports-list-config.service.ts
new file mode 100644
index 0000000000..8a78ae6137
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-reports-list-config.service.ts
@@ -0,0 +1,179 @@
+import { AnalysisReportsDataSource } from './analysis-reports-list-source';
+import { Injectable, NgZone } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Store } from '@ngrx/store';
+import * as moment from 'moment';
+import { of } from 'rxjs';
+
+import { ListView } from '../../../../../store/src/actions/list.actions';
+import { AppState } from '../../../../../store/src/app-state';
+import { ITableColumn } from '../../../shared/components/list/list-table/table.types';
+import { IListConfig, IListMultiFilterConfig, ListViewTypes, IListAction } from '../../../shared/components/list/list.component.types';
+import { defaultHelmKubeListPageSize } from '../../kubernetes/list-types/kube-helm-list-types';
+import { AnalysisReport } from '../store/kube.types';
+import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service';
+import { KubernetesAnalysisService } from '../services/kubernetes.analysis.service';
+import { AnalysisStatusCellComponent } from './analysis-status-cell/analysis-status-cell.component';
+
+@Injectable()
+export class AnalysisReportsListConfig implements IListConfig {
+ AppsDataSource: AnalysisReportsDataSource;
+ isLocal = true;
+ multiFilterConfigs: IListMultiFilterConfig[];
+
+ guid: string;
+
+ columns: Array> = [
+ {
+ columnId: 'name', headerCell: () => 'Name',
+ cellDefinition: {
+ getValue: (row: AnalysisReport) => row.name,
+ getLink: row => `/kubernetes/${this.guid}/analysis/report/${row.id}`
+
+ },
+ sort: {
+ type: 'sort',
+ orderKey: 'name',
+ field: 'name'
+ },
+ cellFlex: '2',
+ },
+ {
+ columnId: 'type',
+ headerCell: () => 'Type',
+ cellDefinition: {
+ getValue: (row: AnalysisReport) => row.type.charAt(0).toUpperCase() + row.type.substring(1)
+ },
+ sort: {
+ type: 'sort',
+ orderKey: 'type',
+ field: 'type'
+ },
+ cellFlex: '1'
+ },
+ {
+ columnId: 'age',
+ headerCell: () => 'Age',
+ cellDefinition: {
+ getValue: (row: AnalysisReport) => {
+ return moment(row.created).fromNow(true);
+ }
+ },
+ sort: {
+ type: 'sort',
+ orderKey: 'age',
+ field: 'created'
+ },
+ cellFlex: '1'
+ },
+ {
+ columnId: 'status',
+ headerCell: () => 'Status',
+ cellComponent: AnalysisStatusCellComponent,
+ // cellDefinition: {
+ // getValue: (row: AnalysisReport) => row.status
+ // },
+ sort: {
+ type: 'sort',
+ orderKey: 'status',
+ field: 'status'
+ },
+ cellFlex: '1'
+ }
+
+ // {
+ // columnId: 'description', headerCell: () => 'Description',
+ // cellDefinition: {
+ // getValue: (row) => row.attributes.description,
+ // },
+ // sort: {
+ // type: 'sort',
+ // orderKey: 'description',
+ // field: 'attributes.description'
+ // },
+ // cellFlex: '5',
+ // },
+ // {
+ // columnId: 'repository', headerCell: () => 'Repository',
+ // cellDefinition: {
+ // getValue: (row) => row.attributes.repo.name
+ // },
+ // sort: {
+ // type: 'sort',
+ // orderKey: 'repository',
+ // field: 'attributes.repo.name'
+ // },
+ // cellFlex: '2',
+ // },
+ ];
+
+ pageSizeOptions = defaultHelmKubeListPageSize;
+ viewType = ListViewTypes.TABLE_ONLY;
+ defaultView = 'table' as ListView;
+
+ enableTextFilter = true;
+ text = {
+ filter: 'Filter by Name',
+ noEntries: 'There are no Analysis Reports'
+ };
+
+ constructor(
+ store: Store,
+ kubeEndpointService: KubernetesEndpointService,
+ private route: ActivatedRoute,
+ private analysisService: KubernetesAnalysisService,
+ ngZone: NgZone,
+ ) {
+ this.guid = kubeEndpointService.baseKube.guid;
+ this.AppsDataSource = new AnalysisReportsDataSource(store, this, kubeEndpointService, ngZone);
+ }
+
+ private listActionDelete: IListAction = {
+ action: (item) => {
+ console.log(item);
+ return this.analysisService.delete(item);
+
+ },
+ label: 'Delete',
+ icon: 'delete',
+ description: ``, // Description depends on console user permission
+ createEnabled: row$ => of(true)
+ };
+
+ private singleActions = [
+ this.listActionDelete,
+ ];
+
+ getGlobalActions = () => [];
+ getMultiActions = () => [];
+ getSingleActions = () => this.singleActions;
+ getColumns = () => this.columns;
+ getDataSource = () => this.AppsDataSource;
+ getMultiFiltersConfigs = () => [];
+ //[this.createRepositoryFilterConfig()];
+
+ // private createRepositoryFilterConfig(): IListMultiFilterConfig {
+ // return {
+ // key: 'repository',
+ // label: 'Repository',
+ // allLabel: 'All Repositories',
+ // list$: this.helmRepositories(),
+ // loading$: observableOf(false),
+ // select: new BehaviorSubject(this.route.snapshot.params.repo)
+ // };
+ // }
+
+ // private helmRepositories(): Observable {
+ // return this.endpointsService.endpoints$.pipe(
+ // map(endpoints => {
+ // const repos = [];
+ // Object.values(endpoints).forEach(ep => {
+ // if (ep.cnsi_type === 'helm') {
+ // repos.push({ label: ep.name, item: ep.name, value: ep.name });
+ // }
+ // });
+ // return repos;
+ // })
+ // );
+ // }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/list-types/analysis-reports-list-source.ts b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-reports-list-source.ts
new file mode 100644
index 0000000000..815c0e5bef
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-reports-list-source.ts
@@ -0,0 +1,57 @@
+import { interval } from 'rxjs';
+import { NgZone } from '@angular/core';
+import { Store } from '@ngrx/store';
+
+import { AppState } from '../../../../../store/src/app-state';
+import { ListDataSource } from '../../../shared/components/list/data-sources-controllers/list-data-source';
+import { IListConfig } from '../../../shared/components/list/list.component.types';
+import { AnalysisReport } from '../store/kube.types';
+import { analysisReportEntityType, kubernetesEntityFactory } from '../kubernetes-entity-factory';
+import { GetAnalysisReports } from '../store/kubernetes.actions';
+import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service';
+import { safeUnsubscribe } from '../../../core/utils.service';
+
+export class AnalysisReportsDataSource extends ListDataSource {
+
+ private polls;
+
+ constructor(
+ store: Store,
+ listConfig: IListConfig,
+ endpointService: KubernetesEndpointService,
+ ngZone: NgZone,
+ ) {
+ const action = new GetAnalysisReports(endpointService.baseKube.guid);
+ super({
+ store,
+ action,
+ schema: kubernetesEntityFactory(analysisReportEntityType),
+ getRowUniqueId: (entity: AnalysisReport) => entity.id,
+ paginationKey: action.paginationKey,
+ isLocal: true,
+ listConfig,
+ //transformEntities: [{ type: 'filter', field: 'name' },
+ // (entities: AnalysisReport[], paginationState: PaginationEntityState) => {
+ // const repository = paginationState.clientPagination.filter.items.repository;
+ // return entities.filter(e => {
+ // return !(repository && repository !== e.attributes.repo.name);
+ // });
+ // ]
+ });
+
+ this.polls = [];
+ ngZone.runOutsideAngular(() => {
+ this.polls.push(
+ interval(5000).subscribe(() => {
+ ngZone.run(() => {
+ store.dispatch(action);
+ });
+ })
+ );
+ });
+ }
+
+ destroy() {
+ safeUnsubscribe(...(this.polls || []));
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.html b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.html
new file mode 100644
index 0000000000..a48aed788e
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.html
@@ -0,0 +1,5 @@
+
+ Running
+
+Completed
+Error
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.scss b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.scss
new file mode 100644
index 0000000000..00d1df35cf
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.scss
@@ -0,0 +1,10 @@
+.status {
+ &__running {
+ align-items: center;
+ display: flex;
+
+ &> mat-progress-spinner {
+ margin-right: 8px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.spec.ts
new file mode 100644
index 0000000000..3c1fb10915
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.spec.ts
@@ -0,0 +1,32 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MDAppModule } from './../../../../core/md.module';
+import { AnalysisStatusCellComponent } from './analysis-status-cell.component';
+import {
+ AnalysisReportSelectorComponent
+} from './../../analysis-report-viewer/analysis-report-selector/analysis-report-selector.component';
+
+describe('AnalysisStatusCellComponent', () => {
+ let component: AnalysisStatusCellComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ AnalysisStatusCellComponent, AnalysisReportSelectorComponent ],
+ imports: [
+ MDAppModule,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AnalysisStatusCellComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.ts b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.ts
new file mode 100644
index 0000000000..acc74ee6d6
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.ts
@@ -0,0 +1,16 @@
+import { Component } from '@angular/core';
+import { TableCellCustom } from 'frontend/packages/core/src/shared/components/list/list.types';
+
+@Component({
+ selector: 'app-analysis-status-cell',
+ templateUrl: './analysis-status-cell.component.html',
+ styleUrls: ['./analysis-status-cell.component.scss']
+})
+export class AnalysisStatusCellComponent extends TableCellCustom {
+
+ constructor() {
+ super();
+ this.row = {};
+ }
+
+ }
diff --git a/custom-src/frontend/app/custom/kubernetes/list-types/kubernetes-labels-cell/kubernetes-labels-cell.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/list-types/kubernetes-labels-cell/kubernetes-labels-cell.component.spec.ts
index 4c332d1f29..1be4bd13b0 100644
--- a/custom-src/frontend/app/custom/kubernetes/list-types/kubernetes-labels-cell/kubernetes-labels-cell.component.spec.ts
+++ b/custom-src/frontend/app/custom/kubernetes/list-types/kubernetes-labels-cell/kubernetes-labels-cell.component.spec.ts
@@ -1,8 +1,8 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { KubernetesStatus } from '../../../../../../../../../custom-src/frontend/app/custom/kubernetes/store/kube.types';
import { BaseTestModules } from '../../../../../test-framework/core-test.helper';
import { KubernetesLabelsCellComponent } from './kubernetes-labels-cell.component';
+import { KubernetesStatus } from '../../store/kube.types';
describe('KubernetesLabelsCellComponent', () => {
let component: KubernetesLabelsCellComponent;
diff --git a/custom-src/frontend/app/custom/kubernetes/services/analysis-report.types.ts b/custom-src/frontend/app/custom/kubernetes/services/analysis-report.types.ts
new file mode 100644
index 0000000000..16f51126bd
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/services/analysis-report.types.ts
@@ -0,0 +1,22 @@
+export enum ResourceAlertLevel {
+ OK = 0,
+ Info,
+ Warning,
+ Error,
+ Unknown,
+}
+
+// We er-map an analysis reprot into a map of resource alerts that is better for us
+// to overlay in the UI to show issues from reports
+export interface ResourceAlert {
+ apiVersion?: string;
+ kind: string;
+ message: string;
+ namespace: string;
+ name: string;
+ level: ResourceAlertLevel;
+}
+
+export interface ResourceAlertMap {
+ [key: string]: ResourceAlert[];
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/services/kubernetes.analysis.service.ts b/custom-src/frontend/app/custom/kubernetes/services/kubernetes.analysis.service.ts
new file mode 100644
index 0000000000..5787c4550d
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/services/kubernetes.analysis.service.ts
@@ -0,0 +1,312 @@
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { MatSnackBar } from '@angular/material';
+import { ActivatedRoute } from '@angular/router';
+import { Store } from '@ngrx/store';
+import { ClearPaginationOfType } from 'frontend/packages/store/src/actions/pagination.actions';
+import { combineLatest, Observable, of } from 'rxjs';
+import { catchError, map, startWith } from 'rxjs/operators';
+
+import { RouterNav } from '../../../../../store/src/actions/router.actions';
+import { AppState } from '../../../../../store/src/app-state';
+import { environment } from '../../../environments/environment';
+import { GetAnalysisReports } from '../store/kubernetes.actions';
+import { KubernetesEndpointService } from './kubernetes-endpoint.service';
+import { KubeScoreReportHelper } from './kubescore-report.helper';
+import { PopeyeReportHelper } from './popeye-report.helper';
+
+export interface KubernetesAnalysisType {
+ name: string;
+ id: string;
+ namespaceAware: boolean;
+ iconUrl?: string;
+ descriptionUrl?: string;
+}
+
+@Injectable()
+export class KubernetesAnalysisService {
+ kubeGuid: string;
+
+ public analyzers$: Observable;
+ public namespaceAnalyzers$: Observable;
+
+ public enabled$: Observable;
+ public hideAnalysis$: Observable;
+
+ constructor(
+ public kubeEndpointService: KubernetesEndpointService,
+ public activatedRoute: ActivatedRoute,
+ public store: Store,
+ public http: HttpClient,
+ private snackBar: MatSnackBar,
+ ) {
+ this.kubeGuid = kubeEndpointService.kubeGuid;
+
+ // Is the backend plugin available?
+ this.enabled$ = this.store.select('auth').pipe(
+ map(auth => auth.sessionData.plugins && auth.sessionData.plugins.analysis)
+ );
+
+ this.hideAnalysis$ = this.enabled$.pipe(
+ startWith(true),
+ map(ok => !ok)
+ );
+
+ this.analyzers$ = of([
+ {
+ name: 'PopEye',
+ id: 'popeye',
+ namespaceAware: true,
+ iconUrl: '/core/assets/custom/popeye.png',
+ iconWidth: '80',
+ descriptionUrl: '/core/assets/custom/popeye.md'
+ },
+ {
+ name: 'Kube Score',
+ id: 'kube-score',
+ namespaceAware: true,
+ iconUrl: '/core/assets/custom/kubescore.png',
+ iconWidth: '120',
+ descriptionUrl: '/core/assets/custom/kubescore.md'
+ },
+ {
+ name: 'Clair',
+ id: 'clair',
+ namespaceAware: true,
+ iconUrl: '/core/assets/custom/clair.png',
+ iconWidth: '70',
+ descriptionUrl: '/core/assets/custom/clair.md'
+ }
+
+ // {
+ // name: 'Sonobuoy',
+ // id: 'sonobuoy',
+ // namespaceAware: false,
+ // iconUrl: '/core/assets/custom/sonobuoy.png',
+ // iconWidth: '70',
+ // descriptionUrl: '/core/assets/custom/sonobuoy.md'
+ // }
+ ]);
+
+ this.namespaceAnalyzers$ = combineLatest(
+ this.analyzers$,
+ this.enabled$
+ ).pipe(
+ map(([a, enabled]) => {
+ if (!enabled) {
+ return null;
+ }
+ return a.filter(v => v.namespaceAware);
+ })
+ );
+ }
+
+ public delete(item) {
+ if (!Array.isArray(item)) {
+ item = [item];
+ }
+
+ const ids = [];
+ item.forEach(i => ids.push(i.id));
+
+ const proxyAPIVersion = environment.proxyAPIVersion;
+
+ // Fetch the report
+ const url = `/pp/${proxyAPIVersion}/analysis/reports`;
+ const headers = new HttpHeaders({});
+ const requestArgs = {
+ headers,
+ body: ids
+ };
+
+ const del = this.http.delete(url, requestArgs);
+ del.subscribe(d => {
+ const action = new GetAnalysisReports(this.kubeEndpointService.baseKube.guid);
+
+ this.store.dispatch(new ClearPaginationOfType(action));
+ this.store.dispatch(action);
+
+ });
+
+ return del;
+ }
+
+ public refresh() {
+ const action = new GetAnalysisReports(this.kubeEndpointService.baseKube.guid);
+ this.store.dispatch(new ClearPaginationOfType(action));
+ this.store.dispatch(action);
+ }
+
+ public run(id: string, endpointID: string, namespace?: string, app?: string) {
+ const proxyAPIVersion = environment.proxyAPIVersion;
+ const body = {
+ namespace,
+ app,
+ };
+
+ // Start an Analysis
+ const url = `/pp/${proxyAPIVersion}/analysis/run/${id}/${endpointID}`;
+ const headers = new HttpHeaders({});
+ const requestArgs = {
+ headers,
+ };
+
+ const start = this.http.post(url, body, requestArgs).pipe(
+ map(response => {
+ return response;
+ }),
+ catchError((e, c) => {
+ console.log('Error occurred');
+ console.log(e);
+ const msg = { firstLine: 'Failed to run Analysis Report'};
+ return of(false);
+ })
+ );
+
+ start.subscribe(a => {
+ const type = id.charAt(0).toUpperCase() + id.substring(1);
+ let msg;
+ if (app) {
+ msg = `${type} analysis started for workload '${app}'`;
+ } else if (namespace) {
+ msg = `${type} analysis started for namespace '${namespace}'`;
+ } else {
+ msg = `${type} analysis started for the Kubernetes cluster`;
+ }
+ const ref = this.snackBar.open(msg, 'View', { duration: 5000 });
+ ref.onAction().subscribe(() => {
+ this.store.dispatch(new RouterNav({ path: ['kubernetes', endpointID, 'analysis'] }));
+ });
+ this.refresh();
+ });
+ }
+
+ public getLatestCheck(endpointID: string, path: string): Observable {
+ return this.getLatestObservable(endpointID, path, true).pipe(
+ map(response => response !== false),
+ );
+ }
+ public getLatest(endpointID: string, path: string): Observable {
+
+ const start = this.getLatestObservable(endpointID, path, false).pipe(
+ map(response => {
+ this.processReport(response);
+ return response;
+ }),
+ catchError((e, c) => {
+ console.log('Error occurred');
+ console.log(e);
+ const msg = { firstLine: 'Failed to run Analysis Report'};
+ return of(false);
+ })
+ );
+
+ return start;
+ }
+
+ private getLatestObservable(endpointID: string, path: string, checkExists = false): Observable {
+ const proxyAPIVersion = environment.proxyAPIVersion;
+ const url = `/pp/${proxyAPIVersion}/analysis/latest/${endpointID}/${path}`;
+ const headers = new HttpHeaders({});
+ const requestArgs = {
+ headers,
+ };
+
+ let req;
+ if (checkExists) {
+ req = this.http.head(url, requestArgs);
+ } else {
+ req = this.http.get(url, requestArgs);
+ }
+
+ return req.pipe(
+ catchError((e, c) => {
+ console.log('Error occurred');
+ console.log(e);
+ const msg = { firstLine: 'Failed to run Analysis Report'};
+ return of(false);
+ })
+ );
+
+ }
+
+ private processReport(report: any) {
+ // Check the path of the report
+ if (report.path.split('/').length !== 2) {
+ return;
+ }
+
+ switch (report.format) {
+ case 'popeye':
+ const helper = new PopeyeReportHelper(report);
+ helper.map();
+ break;
+ case 'kubescore':
+ const kubeScoreHelper = new KubeScoreReportHelper(report);
+ kubeScoreHelper.map();
+ break;
+ default:
+ console.log('Do not know how to handle this report type');
+ break;
+ }
+ }
+
+ public getByID(id: string): Observable {
+ const proxyAPIVersion = environment.proxyAPIVersion;
+ const url = `/pp/${proxyAPIVersion}/analysis/reports/${id}`;
+ const headers = new HttpHeaders({});
+ const requestArgs = {
+ headers,
+ };
+
+ return this.http.get(url, requestArgs).pipe(
+ map(response => {
+ this.processReport(response);
+ return response;
+ }),
+ catchError((e, c) => {
+ console.log('Error occurred');
+ console.log(e);
+ const msg = { firstLine: 'Failed to get Analysis Report'};
+ return of(false);
+ })
+ );
+ }
+
+ public getByPath(endpointID: string, path: string): Observable {
+ const proxyAPIVersion = environment.proxyAPIVersion;
+ const url = `/pp/${proxyAPIVersion}/analysis/completed/${endpointID}/${path}`;
+ const headers = new HttpHeaders({});
+ const requestArgs = {
+ headers,
+ };
+
+ return this.http.get(url, requestArgs).pipe(
+ catchError((e, c) => {
+ console.log('Error occurred');
+ console.log(e);
+ const msg = { firstLine: 'Failed to get Analysis Reports by path'};
+ return of(false);
+ })
+ );
+ }
+
+
+ public getReportFile(id: string, file: string): Observable {
+ const proxyAPIVersion = environment.proxyAPIVersion;
+ const url = `/pp/${proxyAPIVersion}/analysis/reports/${id}/${file}`;
+ const headers = new HttpHeaders({});
+ const requestArgs = {
+ headers,
+ };
+
+ return this.http.get(url, requestArgs).pipe(
+ catchError((e, c) => {
+ console.log('Error occurred');
+ console.log(e);
+ const msg = { firstLine: 'Failed to get Analysis Report'};
+ return of(false);
+ })
+ );
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/services/kubescore-report.helper.ts b/custom-src/frontend/app/custom/kubernetes/services/kubescore-report.helper.ts
new file mode 100644
index 0000000000..b07c0e7abf
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/services/kubescore-report.helper.ts
@@ -0,0 +1,57 @@
+import { ResourceAlert, ResourceAlertLevel, ResourceAlertMap } from './analysis-report.types';
+
+export class KubeScoreReportHelper {
+
+ constructor(public report: any) {}
+
+ public map() {
+ if (!this.report.report) {
+ return;
+ }
+
+ const kubescore = this.report.report;
+ // Go through the report and re-map
+ const result = {} as ResourceAlertMap;
+
+ Object.keys(kubescore).forEach(key => {
+ const item = kubescore[key];
+ let id = item.TypeMeta.kind.toLowerCase();
+ id = `${id}/${item.ObjectMeta.namespace}/${item.ObjectMeta.name}`;
+
+ item.Checks.forEach(check => {
+ if (check.Grade !== 10 && !check.Skipped) {
+ // Add an alert for each comment
+ check.Comments.forEach(comment => {
+ // Include this comment
+ const alert = {
+ kind: item.TypeMeta.kind.toLowerCase(),
+ namespace: item.ObjectMeta.namespace,
+ name: item.ObjectMeta.name,
+ message: comment.Summary,
+ level: this.convertMessageLevel(check.Grade)
+ } as ResourceAlert;
+ if (!result[id]) {
+ result[id] = [] as ResourceAlert[];
+ }
+ result[id].push(alert);
+ });
+ }
+ });
+ });
+ this.report.alerts = result;
+ }
+ private convertMessageLevel(level: number): ResourceAlertLevel {
+ switch (level) {
+ case 10:
+ return ResourceAlertLevel.OK;
+ case 7:
+ return ResourceAlertLevel.Info;
+ case 5:
+ return ResourceAlertLevel.Warning;
+ case 1:
+ return ResourceAlertLevel.Error;
+ default:
+ return ResourceAlertLevel.Unknown;
+ }
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/services/popeye-report.helper.ts b/custom-src/frontend/app/custom/kubernetes/services/popeye-report.helper.ts
new file mode 100644
index 0000000000..082fbc4770
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/services/popeye-report.helper.ts
@@ -0,0 +1,69 @@
+import { ResourceAlertMap, ResourceAlert, ResourceAlertLevel } from './analysis-report.types';
+
+export class PopeyeReportHelper {
+
+ constructor(public report: any) {}
+
+ // Map the report to the alert format
+ public map() {
+ if (!this.report.report || !this.report.report.popeye) {
+ return;
+ }
+
+ const popeye = this.report.report.popeye;
+ // Go through the report and re-map
+ const result = {} as ResourceAlertMap;
+ popeye.sanitizers.forEach(s => {
+ // We just care about issues
+ const resourceType = s.sanitizer;
+ if (s.issues) {
+ Object.keys(s.issues).forEach(resourcePath => {
+ const issues = s.issues[resourcePath];
+ issues.forEach(issue => {
+ // Level must be greater than 0 (OK)
+ if (issue.level > 0) {
+ let namespace;
+ let name;
+ if (resourcePath.indexOf('/') !== -1) {
+ // Has a namespace
+ namespace = resourcePath.split('/')[0];
+ name = resourcePath.split('/')[1];
+ } else {
+ name = resourcePath;
+ namespace = '';
+ }
+ const alert = {
+ kind: resourceType,
+ namespace,
+ name,
+ message: issue.message,
+ level: this.convertMessageLevel(issue.level)
+ } as ResourceAlert;
+ const id = `${resourceType}/${resourcePath}`;
+ if (!result[id]) {
+ result[id] = [] as ResourceAlert[];
+ }
+ result[id].push(alert);
+ }
+ });
+ });
+ }
+ });
+
+ this.report.alerts = result;
+ }
+ private convertMessageLevel(level: number): ResourceAlertLevel {
+ switch (level) {
+ case 0:
+ return ResourceAlertLevel.OK;
+ case 1:
+ return ResourceAlertLevel.Info;
+ case 2:
+ return ResourceAlertLevel.Warning;
+ case 3:
+ return ResourceAlertLevel.Error;
+ default:
+ return ResourceAlertLevel.Unknown;
+ }
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/store/kube.types.ts b/custom-src/frontend/app/custom/kubernetes/store/kube.types.ts
index b6b2721e86..9c59a56af7 100644
--- a/custom-src/frontend/app/custom/kubernetes/store/kube.types.ts
+++ b/custom-src/frontend/app/custom/kubernetes/store/kube.types.ts
@@ -484,3 +484,18 @@ export interface HostPath {
export interface Item {
key: string;
}
+
+
+// Analysis Reports
+
+export interface AnalysisReport {
+ id: string;
+ endpoint: string;
+ type: string;
+ name: string;
+ path: string;
+ created: Date;
+ read: boolean;
+ status: string;
+ duration: number;
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/store/kubernetes.actions.ts b/custom-src/frontend/app/custom/kubernetes/store/kubernetes.actions.ts
index 6d1d44c4ef..f8047b2737 100644
--- a/custom-src/frontend/app/custom/kubernetes/store/kubernetes.actions.ts
+++ b/custom-src/frontend/app/custom/kubernetes/store/kubernetes.actions.ts
@@ -16,7 +16,9 @@ import {
kubernetesPodsEntityType,
kubernetesServicesEntityType,
kubernetesStatefulSetsEntityType,
+ analysisReportEntityType,
} from '../kubernetes-entity-factory';
+import { MonocularPaginationAction } from '../../helm/store/helm.actions';
export const GET_RELEASE_POD_INFO = '[KUBERNETES Endpoint] Get Release Pods Info';
export const GET_RELEASE_POD_INFO_SUCCESS = '[KUBERNETES Endpoint] Get Release Pods Info Success';
@@ -80,6 +82,9 @@ export const GET_KUBE_DASHBOARD = '[KUBERNETES Endpoint] Get K8S Dashboard Info'
export const GET_KUBE_DASHBOARD_SUCCESS = '[KUBERNETES Endpoint] Get Dashboard Success';
export const GET_KUBE_DASHBOARD_FAILURE = '[KUBERNETES Endpoint] Get Dashboard Failure';
+export const GET_ANALYSIS_REPORTS = '[ANALYSIS] Get Reports';
+export const GET_ANALYSIS_REPORTS_SUCCESS = '[ANALYSIS] Get Reports Success';
+export const GET_ANALYSIS_REPORTS_FAILURE = '[ANALYSIS] Get Reports Failure';
const sortPodsByName = {
'order-direction': 'desc' as SortDirection,
@@ -384,4 +389,25 @@ export class FetchKubernetesChartMetricsAction extends MetricsChartAction {
}
}
+//export interface AnalysisPaginationAction extends PaginatedAction, EntityRequestAction { }
+// Get the analysis reports for the given endpoint ID
+export class GetAnalysisReports implements MonocularPaginationAction {
+ constructor(public endpointId: string) {
+ this.paginationKey = `k8s-${endpointId}`;
+ }
+ type = GET_ANALYSIS_REPORTS;
+ endpointType = KUBERNETES_ENDPOINT_TYPE;
+ entityType = analysisReportEntityType;
+ entity = [kubernetesEntityFactory(analysisReportEntityType)];
+ actions = [
+ GET_ANALYSIS_REPORTS,
+ GET_ANALYSIS_REPORTS_SUCCESS,
+ GET_ANALYSIS_REPORTS_FAILURE
+ ];
+ paginationKey: string;
+ initialParams = {
+ 'order-direction': 'desc',
+ 'order-direction-field': 'name',
+ };
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/store/kubernetes.effects.ts b/custom-src/frontend/app/custom/kubernetes/store/kubernetes.effects.ts
index d5d36df44d..eee1d8f86b 100644
--- a/custom-src/frontend/app/custom/kubernetes/store/kubernetes.effects.ts
+++ b/custom-src/frontend/app/custom/kubernetes/store/kubernetes.effects.ts
@@ -27,6 +27,7 @@ import {
kubernetesPodsEntityType,
kubernetesServicesEntityType,
kubernetesStatefulSetsEntityType,
+ analysisReportEntityType,
} from '../kubernetes-entity-factory';
import { KubernetesPodExpandedStatusHelper } from '../services/kubernetes-expanded-state';
import { getKubeAPIResourceGuid } from './kube.selectors';
@@ -71,6 +72,7 @@ import {
KubeAction,
KubePaginationAction,
} from './kubernetes.actions';
+import { GetAnalysisReports, GET_ANALYSIS_REPORTS } from '../store/kubernetes.actions';
export interface KubeDashboardContainer {
name: string;
@@ -105,6 +107,38 @@ export class KubernetesEffects {
constructor(private http: HttpClient, private actions$: Actions, private store: Store) { }
+ // Analysis reports
+ @Effect()
+ fetchAnalysisReports$ = this.actions$.pipe(
+ ofType(GET_ANALYSIS_REPORTS),
+ flatMap(action => {
+ this.store.dispatch(new StartRequestAction(action));
+ const headers = new HttpHeaders({});
+ const requestArgs = {
+ headers
+ };
+ const url = `/pp/${this.proxyAPIVersion}/analysis/reports`;
+ const entityConfig = entityCatalog.getEntity(KUBERNETES_ENDPOINT_TYPE, analysisReportEntityType);
+ const entityKey = entityCatalog.getEntityKey(action);
+ return this.http
+ .get(url, requestArgs)
+ .pipe(mergeMap(response => {
+ const res = {
+ entities: { [entityKey]: {} },
+ result: []
+ } as NormalizedResponse;
+ const items = response as Array;
+ items.forEach(item => {
+ const id = item.id;
+ res.entities[entityKey][id] = item;
+ res.result.push(id);
+ });
+ return [new WrapperRequestActionSuccess(res, action)];
+ })
+ );
+ })
+ );
+
@Effect()
fetchDashboardInfo$ = this.actions$.pipe(
ofType(GET_KUBE_DASHBOARD),
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.html b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.html
new file mode 100644
index 0000000000..8a0f741191
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.html
@@ -0,0 +1,4 @@
+
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.scss b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.scss
new file mode 100644
index 0000000000..33613315cb
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.scss
@@ -0,0 +1,26 @@
+.info {
+
+ &__card {
+ display: flex;
+
+ h1 {
+ font-size: 16px;
+ padding: 0;
+ }
+ }
+
+ &__card-icon {
+ flex: 0 0 140px;
+ width: 140px;
+ &>img {
+ max-width: 120px;
+ }
+ margin-right: 10px;
+ text-align: center;
+ }
+
+ &__card-text {
+ flex: 1;
+ }
+
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.spec.ts
new file mode 100644
index 0000000000..88894b763d
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.spec.ts
@@ -0,0 +1,29 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AnalysisInfoCardComponent } from './analysis-info-card.component';
+
+describe('AnalysisInfoCardComponent', () => {
+ let component: AnalysisInfoCardComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ AnalysisInfoCardComponent ],
+ imports: [
+ HttpClientTestingModule,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AnalysisInfoCardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.ts b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.ts
new file mode 100644
index 0000000000..1d15eba9d3
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.ts
@@ -0,0 +1,55 @@
+import { Observable, of } from 'rxjs';
+import { Component, Input } from '@angular/core';
+import * as markdown from 'marked';
+import { HttpClient } from '@angular/common/http';
+import { catchError, map } from 'rxjs/operators';
+
+@Component({
+ selector: 'app-analysis-info-card',
+ templateUrl: './analysis-info-card.component.html',
+ styleUrls: ['./analysis-info-card.component.scss']
+})
+export class AnalysisInfoCardComponent {
+
+ public loading = true;
+ public content$: Observable;
+ private renderer = new markdown.Renderer();
+
+ public mAanalyzer = {};
+
+ @Input() set analyzer(analyzer: any) {
+ if (analyzer && analyzer.descriptionUrl) {
+ this.content$ = this.getDescription(analyzer.descriptionUrl);
+ }
+ this.mAanalyzer = analyzer;
+ }
+
+ get analyzer() {
+ return this.mAanalyzer;
+ }
+
+ constructor(private http: HttpClient) {
+ this.renderer.link = (href, title, text) => `${text}`;
+ this.renderer.code = (text: string) => `${text}
`;
+ }
+
+ private getDescription(url): Observable {
+ return this.http.get(url, { responseType: 'text' }).pipe(
+ map(resp => {
+ this.loading = false;
+ return markdown(resp, {
+ renderer: this.renderer
+ });
+ }),
+ catchError((error) => {
+ this.loading = false;
+ if (error.status === 404) {
+ return of('Unable to load description for this Analyzer
');
+ } else {
+ return of('An error occurred retrieving description for this Analyzer
');
+ }
+ }
+ ));
+ }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.html b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.html
new file mode 100644
index 0000000000..45330537ae
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.html
@@ -0,0 +1,10 @@
+
+ Available Analyzers
+
+
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.scss b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.scss
new file mode 100644
index 0000000000..53951f99ae
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.scss
@@ -0,0 +1,5 @@
+.info__title {
+ padding: 0;
+ margin: 0 0 30px 0;
+ font-size: 22px;
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.spec.ts
new file mode 100644
index 0000000000..0a35b86689
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.spec.ts
@@ -0,0 +1,38 @@
+
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { KubernetesAnalysisInfoComponent } from './kubernetes-analysis-info.component';
+import { AnalysisInfoCardComponent } from './analysis-info-card/analysis-info-card.component';
+import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../../kubernetes.testing.module';
+import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../../services/kubernetes-endpoint.service';
+
+describe('KubernetesAnalysisInfoComponent', () => {
+ let component: KubernetesAnalysisInfoComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ KubernetesAnalysisInfoComponent, AnalysisInfoCardComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+ ],
+ providers: [
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(KubernetesAnalysisInfoComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.ts b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.ts
new file mode 100644
index 0000000000..f7d2d9b2e7
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.ts
@@ -0,0 +1,17 @@
+
+import { Component } from '@angular/core';
+import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service';
+
+@Component({
+ selector: 'app-kubernetes-analysis-info',
+ templateUrl: './kubernetes-analysis-info.component.html',
+ styleUrls: ['./kubernetes-analysis-info.component.scss'],
+ providers: [
+ KubernetesAnalysisService
+ ]
+})
+export class KubernetesAnalysisInfoComponent {
+
+ constructor(public analysisService: KubernetesAnalysisService) { }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.html b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.html
new file mode 100644
index 0000000000..823c289ef7
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.scss b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.scss
new file mode 100644
index 0000000000..f70ee0c2c3
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.scss
@@ -0,0 +1,43 @@
+.report {
+ &__report-header {
+ align-items: center;
+ display: flex;
+ margin-bottom: 8px;
+ }
+ &__header {
+ align-items: center;
+ display: flex;
+ }
+ &__title {
+ flex: 1;
+ }
+ &__stat {
+ display: flex;
+ flex-direction: column;
+ padding: 5px 12px;
+ &>div:first-child {
+ opacity: 0.8;
+ }
+ }
+ &__score {
+ flex: 0;
+ font-size: 20px;
+ }
+ &__grade {
+ flex: 0;
+ font-size: 20px;
+ }
+ &__table {
+ margin-left: 20px;
+ }
+ &__issue {
+ align-items: center;
+ display: flex;
+ }
+ &__icon {
+ padding-right: 4px;
+ }
+ &__table-name {
+ vertical-align: top;
+ }
+}
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.spec.ts
new file mode 100644
index 0000000000..f1f2d4fd8c
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.spec.ts
@@ -0,0 +1,32 @@
+
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { KubernetesAnalysisReportComponent } from './kubernetes-analysis-report.component';
+import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module';
+import { AnalysisReportViewerComponent } from './../../../analysis-report-viewer/analysis-report-viewer.component';
+
+describe('KubernetesAnalysisReportComponent', () => {
+ let component: KubernetesAnalysisReportComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ KubernetesAnalysisReportComponent, AnalysisReportViewerComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+// MDAppModule
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(KubernetesAnalysisReportComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.theme.scss b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.theme.scss
new file mode 100644
index 0000000000..4ea5002e16
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.theme.scss
@@ -0,0 +1,13 @@
+
+@mixin kube-analysis-report-theme($theme, $app-theme) {
+ $backgrounds: map-get($theme, background);
+ $background: mat-color($backgrounds, card);
+ $background-color: map-get($app-theme, app-background-color);
+ $darker-background-color: darken($background-color, 4%);
+ .report__header {
+ background-color: $darker-background-color;
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ padding-left: 10px;
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.ts b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.ts
new file mode 100644
index 0000000000..0aa648c74e
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.ts
@@ -0,0 +1,55 @@
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Observable, of, Subject } from 'rxjs';
+import { catchError, map, startWith } from 'rxjs/operators';
+
+import { environment } from '../../../../../environments/environment';
+
+@Component({
+ selector: 'app-kubernetes-analysis-report',
+ templateUrl: './kubernetes-analysis-report.component.html',
+ styleUrls: ['./kubernetes-analysis-report.component.scss']
+})
+export class KubernetesAnalysisReportComponent implements OnInit {
+
+ report$: Observable;
+ private errorMsg = new Subject();
+ errorMsg$ = this.errorMsg.pipe(startWith(''));
+ isLoading$: Observable;
+
+ id: string;
+
+ constructor(public http: HttpClient, route: ActivatedRoute) {
+ const parts = route.snapshot.params;
+ this.id = parts.id;
+ }
+
+ ngOnInit() {
+ const proxyAPIVersion = environment.proxyAPIVersion;
+
+ // Fetch the report
+ const url = `/pp/${proxyAPIVersion}/analysis/reports/${this.id}`;
+ const headers = new HttpHeaders({});
+ const requestArgs = {
+ headers
+ };
+
+ this.report$ = this.http.get(url, requestArgs).pipe(
+ map((response: any) => {
+ this.errorMsg.next('');
+ return response;
+ }),
+ catchError((e, c) => {
+ const msg = { firstLine: 'Failed to load Analysis Report'};
+ this.errorMsg.next(msg);
+ return of(false);
+ })
+ );
+
+ this.isLoading$ = this.report$.pipe(
+ map(() => false),
+ startWith(true)
+ );
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.html b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.html
new file mode 100644
index 0000000000..959090f5c9
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.scss b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.spec.ts
new file mode 100644
index 0000000000..b13da9b832
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.spec.ts
@@ -0,0 +1,42 @@
+
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { MDAppModule } from './../../../../core/md.module';
+
+import { KubernetesAnalysisTabComponent } from './kubernetes-analysis-tab.component';
+import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../kubernetes.testing.module';
+import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service';
+import { AnalysisReportViewerComponent } from './../../analysis-report-viewer/analysis-report-viewer.component';
+import { TabNavService } from 'frontend/packages/core/tab-nav.service';
+
+describe('KubernetesAnalysisTabComponent', () => {
+ let component: KubernetesAnalysisTabComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ KubernetesAnalysisTabComponent, AnalysisReportViewerComponent ],
+ imports: [
+ KubernetesBaseTestModules,
+ MDAppModule
+ ],
+ providers: [
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
+ TabNavService,
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(KubernetesAnalysisTabComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.ts b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.ts
new file mode 100644
index 0000000000..1b36cc9116
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.ts
@@ -0,0 +1,42 @@
+import { Component, OnInit } from '@angular/core';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { map, catchError } from 'rxjs/operators';
+import { of } from 'rxjs';
+
+import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service';
+import { ListConfig } from 'frontend/packages/core/src/shared/components/list/list.component.types';
+import { AnalysisReportsListConfig } from '../../list-types/analysis-reports-list-config.service';
+import { environment } from '../../../../environments/environment';
+import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service';
+
+@Component({
+ selector: 'app-kubernetes-analysis-tab',
+ templateUrl: './kubernetes-analysis-tab.component.html',
+ styleUrls: ['./kubernetes-analysis-tab.component.scss'],
+ providers: [
+ KubernetesAnalysisService,
+ {
+ provide: ListConfig,
+ useClass: AnalysisReportsListConfig,
+ }
+ ]
+})
+export class KubernetesAnalysisTabComponent implements OnInit {
+
+ infoLink: string;
+
+ constructor(
+ public kubeEndpointService: KubernetesEndpointService,
+ public analysisService: KubernetesAnalysisService,
+ public http: HttpClient
+ ) {
+ const guid = this.kubeEndpointService.baseKube.guid;
+ this.infoLink = `/kubernetes/${guid}/analysis/info`;
+ }
+
+ ngOnInit() { }
+
+ public runAnalysis(id: string) {
+ this.analysisService.run(id, this.kubeEndpointService.baseKube.guid);
+ }
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.spec.ts
index 4777afe99b..96996ee411 100644
--- a/custom-src/frontend/app/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.spec.ts
+++ b/custom-src/frontend/app/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.spec.ts
@@ -1,8 +1,10 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TabNavService } from 'frontend/packages/core/tab-nav.service';
-import { HelmReleaseProviders, KubernetesBaseTestModules } from '../../../kubernetes.testing.module';
+import { HelmReleaseProviders, KubernetesBaseTestModules, KubeBaseGuidMock } from '../../../kubernetes.testing.module';
import { HelmReleaseTabBaseComponent } from './helm-release-tab-base.component';
+import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../../services/kubernetes-endpoint.service';
describe('HelmReleaseTabBaseComponent', () => {
@@ -15,7 +17,10 @@ describe('HelmReleaseTabBaseComponent', () => {
declarations: [HelmReleaseTabBaseComponent],
providers: [
...HelmReleaseProviders,
- TabNavService
+ TabNavService,
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
]
})
.compileComponents();
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts b/custom-src/frontend/app/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts
index 0787289ab5..ece28f69cc 100644
--- a/custom-src/frontend/app/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts
+++ b/custom-src/frontend/app/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts
@@ -9,7 +9,7 @@ import { PaginatedAction } from 'frontend/packages/store/src/types/pagination.ty
import { EntityRequestAction, WrapperRequestActionSuccess } from 'frontend/packages/store/src/types/request.types';
import { Observable, Subject, Subscription } from 'rxjs';
import makeWebSocketObservable, { GetWebSocketResponses } from 'rxjs-websockets';
-import { catchError, map, share, switchMap } from 'rxjs/operators';
+import { catchError, map, share, switchMap, startWith } from 'rxjs/operators';
import { KubernetesPodExpandedStatusHelper } from '../../../services/kubernetes-expanded-state';
import { getKubeAPIResourceGuid } from '../../../store/kube.selectors';
@@ -23,6 +23,7 @@ import {
} from '../../store/workloads.actions';
import { HelmReleaseGraph, HelmReleaseGuid, HelmReleasePod } from '../../workload.types';
import { HelmReleaseHelperService } from '../tabs/helm-release-helper.service';
+import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service';
type IDGetterFunction = (data: any) => string;
@@ -34,6 +35,7 @@ type IDGetterFunction = (data: any) => string;
styleUrls: ['./helm-release-tab-base.component.scss'],
providers: [
HelmReleaseHelperService,
+ KubernetesAnalysisService,
{
provide: HelmReleaseGuid,
useFactory: (activatedRoute: ActivatedRoute) => ({
@@ -61,22 +63,28 @@ export class HelmReleaseTabBaseComponent implements OnDestroy {
public title = '';
- tabLinks: IPageSideNavTab[] = [
- { link: 'summary', label: 'Summary', icon: 'helm', iconFont: 'stratos-icons' },
- { link: 'notes', label: 'Notes', icon: 'subject' },
- { link: 'values', label: 'Values', icon: 'list' },
- { link: '-', label: 'Resources' },
- // { link: 'graph', label: 'Overview', icon: 'share' },
- { link: 'pods', label: 'Pods', icon: 'adjust' },
- { link: 'services', label: 'Services', icon: 'service', iconFont: 'stratos-icons' }
- ];
+ tabLinks: IPageSideNavTab[];
+
constructor(
public helmReleaseHelper: HelmReleaseHelperService,
private store: Store,
- private logService: LoggerService
+ private logService: LoggerService,
+ private analysisService: KubernetesAnalysisService,
) {
this.title = this.helmReleaseHelper.releaseTitle;
+ const path = `${this.helmReleaseHelper.namespace}/${this.title}`;
+
+ this.tabLinks = [
+ { link: 'summary', label: 'Summary', icon: 'helm', iconFont: 'stratos-icons' },
+ { link: 'notes', label: 'Notes', icon: 'subject' },
+ { link: 'values', label: 'Values', icon: 'list' },
+ { link: 'analysis', label: 'Analysis', icon: 'assignment', hidden$: this.analysisService.hideAnalysis$ },
+ { link: '-', label: 'Resources' },
+ { link: 'graph', label: 'Overview', icon: 'share' },
+ { link: 'pods', label: 'Pods', icon: 'adjust' },
+ { link: 'services', label: 'Services', icon: 'service', iconFont: 'stratos-icons' }
+ ];
const releaseRef = this.helmReleaseHelper.guidAsUrlFragment();
const host = window.location.host;
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.html b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.html
new file mode 100644
index 0000000000..74acf7da56
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.scss b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.spec.ts
new file mode 100644
index 0000000000..a2c29af062
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.spec.ts
@@ -0,0 +1,42 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HelmReleaseAnalysisTabComponent } from './helm-release-analysis-tab.component';
+import { AnalysisReportSelectorComponent } from './../../../../analysis-report-viewer/analysis-report-selector/analysis-report-selector.component';
+import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../../../kubernetes.testing.module';
+import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../../../services/kubernetes-endpoint.service';
+import { AnalysisReportViewerComponent } from '../../../../analysis-report-viewer/analysis-report-viewer.component';
+import { HelmReleaseProviders } from '../../../../kubernetes.testing.module';
+import { TabNavService } from 'frontend/packages/core/tab-nav.service';
+
+describe('HelmReleaseAnalysisTabComponent', () => {
+ let component: HelmReleaseAnalysisTabComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ HelmReleaseAnalysisTabComponent, AnalysisReportSelectorComponent, AnalysisReportViewerComponent],
+ imports: [
+ KubernetesBaseTestModules,
+ ],
+ providers: [
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
+ HelmReleaseProviders,
+ TabNavService
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HelmReleaseAnalysisTabComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.ts b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.ts
new file mode 100644
index 0000000000..e2c7a24891
--- /dev/null
+++ b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.ts
@@ -0,0 +1,35 @@
+import { Component } from '@angular/core';
+
+import { HelmReleaseHelperService } from '../helm-release-helper.service';
+import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service';
+import { Subject } from 'rxjs';
+import { AnalysisReport } from '../../../../store/kube.types';
+
+@Component({
+ selector: 'app-helm-release-analysis-tab',
+ templateUrl: './helm-release-analysis-tab.component.html',
+ styleUrls: ['./helm-release-analysis-tab.component.scss']
+})
+export class HelmReleaseAnalysisTabComponent {
+
+ public report$ = new Subject();
+
+ path: string;
+
+ currentReport = null;
+
+ constructor(
+ public analaysisService: KubernetesAnalysisService,
+ public helmReleaseHelper: HelmReleaseHelperService
+ ) {
+ this.path = `${this.helmReleaseHelper.namespace}/${this.helmReleaseHelper.releaseTitle}`;
+ }
+
+ public analysisChanged(report) {
+ if (report.id !== this.currentReport) {
+ this.currentReport = report.id;
+ this.analaysisService.getByID(report.id).subscribe(r => this.report$.next(r));
+ }
+ }
+
+}
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.html b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.html
index 6dd8d86bda..74fc91eb8d 100644
--- a/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.html
+++ b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.html
@@ -3,23 +3,35 @@
all_out
Fit
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -58,8 +76,8 @@
@@ -76,12 +94,11 @@
- Loading Resources
+ Loading resources
-
\ No newline at end of file
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts
index 60540500e7..ebe873f1d2 100644
--- a/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts
+++ b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts
@@ -1,8 +1,12 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TabNavService } from 'frontend/packages/core/tab-nav.service';
-import { HelmReleaseProviders, KubernetesBaseTestModules } from '../../../../kubernetes.testing.module';
+import { HelmReleaseProviders, KubernetesBaseTestModules, KubeBaseGuidMock } from '../../../../kubernetes.testing.module';
import { HelmReleaseSummaryTabComponent } from './helm-release-summary-tab.component';
+import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service';
+import { KubernetesEndpointService } from '../../../../services/kubernetes-endpoint.service';
+import { AnalysisReportSelectorComponent } from './../../../../analysis-report-viewer/analysis-report-selector/analysis-report-selector.component';
+import { SidePanelService } from './../../../../../../shared/services/side-panel.service';
describe('HelmReleaseSummaryTabComponent', () => {
let component: HelmReleaseSummaryTabComponent;
@@ -13,10 +17,14 @@ describe('HelmReleaseSummaryTabComponent', () => {
imports: [
...KubernetesBaseTestModules
],
- declarations: [HelmReleaseSummaryTabComponent],
+ declarations: [HelmReleaseSummaryTabComponent, AnalysisReportSelectorComponent],
providers: [
...HelmReleaseProviders,
- TabNavService
+ KubernetesAnalysisService,
+ KubernetesEndpointService,
+ KubeBaseGuidMock,
+ TabNavService,
+ SidePanelService
]
})
.compileComponents();
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts
index 43b268b89a..e3210f28db 100644
--- a/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts
+++ b/custom-src/frontend/app/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts
@@ -1,5 +1,6 @@
+import { ResourceAlert } from './../../../../services/analysis-report.types';
import { HttpClient } from '@angular/common/http';
-import { Component, OnDestroy } from '@angular/core';
+import { Component, OnDestroy, ComponentFactoryResolver } from '@angular/core';
import { Store } from '@ngrx/store';
import { LoggerService } from 'frontend/packages/core/src/core/logger.service';
import { ConfirmationDialogConfig } from 'frontend/packages/core/src/shared/components/confirmation-dialog.config';
@@ -8,18 +9,24 @@ import { ClearPaginationOfType } from 'frontend/packages/store/src/actions/pagin
import { RouterNav } from 'frontend/packages/store/src/actions/router.actions';
import { HideSnackBar, ShowSnackBar } from 'frontend/packages/store/src/actions/snackBar.actions';
import { AppState } from 'frontend/packages/store/src/app-state';
-import { combineLatest, Observable, ReplaySubject } from 'rxjs';
-import { filter, first, map, publishReplay, refCount, startWith } from 'rxjs/operators';
+import { combineLatest, Observable, ReplaySubject, Subject } from 'rxjs';
+import { filter, first, map, publishReplay, refCount, startWith, distinctUntilChanged, take } from 'rxjs/operators';
import { endpointsEntityRequestDataSelector } from '../../../../../../../../store/src/selectors/endpoint.selectors';
import { GetHelmReleases } from '../../../store/workloads.actions';
import { HelmReleaseChartData, HelmReleaseResource } from '../../../workload.types';
import { HelmReleaseHelperService } from '../helm-release-helper.service';
+import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service';
+import { SidePanelService } from 'frontend/packages/core/src/shared/services/side-panel.service';
+import { ResourceAlertPreviewComponent } from '../../../../analysis-report-viewer/resource-alert-preview/resource-alert-preview.component';
@Component({
selector: 'app-helm-release-summary-tab',
templateUrl: './helm-release-summary-tab.component.html',
- styleUrls: ['./helm-release-summary-tab.component.scss']
+ styleUrls: ['./helm-release-summary-tab.component.scss'],
+ providers: [
+ KubernetesAnalysisService,
+ ]
})
export class HelmReleaseSummaryTabComponent implements OnDestroy {
// Confirmation dialogs
@@ -37,6 +44,8 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy {
private successChartColor = '#4DD3A7';
+ public path: string;
+
public podChartColors = [
{
name: 'Running',
@@ -85,14 +94,22 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy {
public chartData$: Observable;
public resources$: Observable;
+ // Cached analysis report
+ private analysisReport;
+
+ private analysisReportUpdated = new Subject();
+ private analysisReportUpdated$ = this.analysisReportUpdated.pipe(startWith(null), distinctUntilChanged());
+
constructor(
+ private componentFactoryResolver: ComponentFactoryResolver,
public helmReleaseHelper: HelmReleaseHelperService,
private store: Store,
private confirmDialog: ConfirmationDialogService,
private httpClient: HttpClient,
- private logService: LoggerService
+ private logService: LoggerService,
+ public analyzerService: KubernetesAnalysisService,
+ private previewPanel: SidePanelService,
) {
-
this.isBusy$ = combineLatest([
this.helmReleaseHelper.isFetching$,
this.busyDeletingSubject.asObservable().pipe(
@@ -103,10 +120,16 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy {
startWith(true)
);
+
+ this.path = `${this.helmReleaseHelper.namespace}/${this.helmReleaseHelper.releaseTitle}`;
+
this.chartData$ = this.helmReleaseHelper.fetchReleaseChartStats();
- this.resources$ = this.helmReleaseHelper.fetchReleaseGraph().pipe(
- map((graph: any) => {
+ this.resources$ = combineLatest(
+ this.helmReleaseHelper.fetchReleaseGraph(),
+ this.analysisReportUpdated$
+ ).pipe(
+ map(([graph, id]) => {
const resources = {};
// Collect the resources
Object.values(graph.nodes).forEach((node: any) => {
@@ -122,12 +145,14 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy {
resources[node.data.kind].count++;
resources[node.data.kind].statuses.push(node.data.status);
});
+ this.applyAnalysis(resources, this.analysisReport);
return Object.values(resources).sort((a: any, b: any) => a.kind.localeCompare(b.kind));
}),
publishReplay(1),
refCount()
);
+
this.hasResources$ = combineLatest([
this.chartData$,
this.resources$
@@ -149,6 +174,26 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy {
},
'Delete'
);
+
+ this.hasAllResources$ = combineLatest([
+ this.resources$,
+ this.hasResources$
+ ]).pipe(
+ map(([resources, hasSome]) => hasSome && resources && resources.length > 0)
+ );
+ }
+
+ public analysisChanged(report) {
+ if (report === null) {
+ // No report selected
+ this.analysisReport = null;
+ this.analysisReportUpdated.next('');
+ } else {
+ this.analyzerService.getByID(report.id).subscribe(results => {
+ this.analysisReport = results;
+ this.analysisReportUpdated.next(report.id);
+ });
+ }
}
private getIcon(kind: string) {
@@ -226,4 +271,41 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy {
first()
);
}
+
+ public runAnalysis(id: string) {
+ this.helmReleaseHelper.release$.pipe(first()).subscribe(release => {
+ this.analyzerService.run(id, this.helmReleaseHelper.endpointGuid, release.namespace, release.name);
+ });
+ }
+
+ private applyAnalysis(resources, report) {
+ // Clear out existing alerts for all resources
+ Object.values(resources).forEach((resource: any) => resource.alerts = []);
+
+ if (report && Object.keys(resources).length > 0) {
+ Object.values(report.alerts).forEach((group: ResourceAlert[]) => {
+ group.forEach(alert => {
+ // Can we find a corresponding group in the resources?
+ const res = Object.keys(resources).find((i) => i.toLowerCase() === alert.kind);
+ if (res) {
+ const resItem = resources[res];
+ if (resItem) {
+ resItem.alerts.push(alert);
+ }
+ }
+ });
+ });
+ }
+ }
+
+ public showAlerts(alerts, resource) {
+ this.previewPanel.show(
+ ResourceAlertPreviewComponent,
+ {
+ resource,
+ alerts,
+ },
+ this.componentFactoryResolver
+ );
+ }
}
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/workloads.module.ts b/custom-src/frontend/app/custom/kubernetes/workloads/workloads.module.ts
index 6514ee1c00..ae34a04e07 100644
--- a/custom-src/frontend/app/custom/kubernetes/workloads/workloads.module.ts
+++ b/custom-src/frontend/app/custom/kubernetes/workloads/workloads.module.ts
@@ -18,6 +18,7 @@ import { HelmReleaseValuesTabComponent } from './release/tabs/helm-release-value
import { HelmReleasesTabComponent } from './releases-tab/releases-tab.component';
import { WorkloadsStoreModule } from './store/workloads.store.module';
import { WorkloadsRouting } from './workloads.routing';
+import { HelmReleaseAnalysisTabComponent } from './release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component';
@NgModule({
imports: [
@@ -39,6 +40,7 @@ import { WorkloadsRouting } from './workloads.routing';
HelmReleaseServicesTabComponent,
HelmReleaseResourceGraphComponent,
HelmReleaseCardComponent,
+ HelmReleaseAnalysisTabComponent,
],
entryComponents: [
HelmReleaseCardComponent
diff --git a/custom-src/frontend/app/custom/kubernetes/workloads/workloads.routing.ts b/custom-src/frontend/app/custom/kubernetes/workloads/workloads.routing.ts
index 8f3a4100a7..dc687ecd76 100644
--- a/custom-src/frontend/app/custom/kubernetes/workloads/workloads.routing.ts
+++ b/custom-src/frontend/app/custom/kubernetes/workloads/workloads.routing.ts
@@ -12,6 +12,7 @@ import { HelmReleaseServicesTabComponent } from './release/tabs/helm-release-ser
import { HelmReleaseSummaryTabComponent } from './release/tabs/helm-release-summary-tab/helm-release-summary-tab.component';
import { HelmReleaseValuesTabComponent } from './release/tabs/helm-release-values-tab/helm-release-values-tab.component';
import { HelmReleasesTabComponent } from './releases-tab/releases-tab.component';
+import { HelmReleaseAnalysisTabComponent } from './release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component';
const routes: Routes = [
{
@@ -36,7 +37,8 @@ const routes: Routes = [
{ path: 'values', component: HelmReleaseValuesTabComponent },
{ path: 'pods', component: HelmReleasePodsTabComponent },
{ path: 'services', component: HelmReleaseServicesTabComponent },
- { path: 'graph', component: HelmReleaseResourceGraphComponent }
+ { path: 'graph', component: HelmReleaseResourceGraphComponent },
+ { path: 'analysis', component: HelmReleaseAnalysisTabComponent },
]
},
]
diff --git a/custom-src/frontend/assets/custom/clair.md b/custom-src/frontend/assets/custom/clair.md
new file mode 100644
index 0000000000..815553cf5d
--- /dev/null
+++ b/custom-src/frontend/assets/custom/clair.md
@@ -0,0 +1,5 @@
+## Clair - Vulnerability Static Analysis for Containers
+
+Clair is an open source project for the static analysis of vulnerabilities in application containers.
+
+[https://github.com/quay/clair](https://github.com/quay/clair)
\ No newline at end of file
diff --git a/custom-src/frontend/assets/custom/clair.png b/custom-src/frontend/assets/custom/clair.png
new file mode 100644
index 0000000000..e8e39243e5
Binary files /dev/null and b/custom-src/frontend/assets/custom/clair.png differ
diff --git a/custom-src/frontend/assets/custom/kubescore.md b/custom-src/frontend/assets/custom/kubescore.md
new file mode 100644
index 0000000000..a4a592e740
--- /dev/null
+++ b/custom-src/frontend/assets/custom/kubescore.md
@@ -0,0 +1,7 @@
+## Kube-Score
+
+Kube-score is a tool that performs static code analysis of your Kubernetes object definitions.
+
+The output is a list of recommendations of what you can improve to make your application more secure and resilient.
+
+[https://github.com/zegl/kube-score](https://github.com/zegl/kube-score)
\ No newline at end of file
diff --git a/custom-src/frontend/assets/custom/kubescore.png b/custom-src/frontend/assets/custom/kubescore.png
new file mode 100644
index 0000000000..ee6b8f82f0
Binary files /dev/null and b/custom-src/frontend/assets/custom/kubescore.png differ
diff --git a/custom-src/frontend/assets/custom/popeye.md b/custom-src/frontend/assets/custom/popeye.md
new file mode 100644
index 0000000000..98f871ba7c
--- /dev/null
+++ b/custom-src/frontend/assets/custom/popeye.md
@@ -0,0 +1,7 @@
+## Popeye - A Kubernetes Cluster Sanitizer
+
+Popeye is a utility that scans live Kubernetes cluster and reports potential issues with deployed resources and configurations. It sanitizes your cluster based on what's deployed and not what's sitting on disk. By scanning your cluster, it detects misconfigurations and ensure best practices are in place thus preventing potential future headaches. It aims at reducing the cognitive overload one faces when operating a Kubernetes cluster in the wild. Furthermore, if your cluster employs a metric-server, it reports potential resources over/under allocations and attempts to warn you should your cluster run out of capacity.
+
+Popeye is a readonly tool, it does not alter any of your Kubernetes resources in any way!
+
+[https://github.com/derailed/popeye](https://github.com/derailed/popeye)
\ No newline at end of file
diff --git a/custom-src/frontend/assets/custom/popeye.png b/custom-src/frontend/assets/custom/popeye.png
new file mode 100644
index 0000000000..8d07a32b96
Binary files /dev/null and b/custom-src/frontend/assets/custom/popeye.png differ
diff --git a/custom-src/frontend/assets/custom/sonobuoy.md b/custom-src/frontend/assets/custom/sonobuoy.md
new file mode 100644
index 0000000000..4e7e951cbc
--- /dev/null
+++ b/custom-src/frontend/assets/custom/sonobuoy.md
@@ -0,0 +1,5 @@
+## Sonobuoy - Validate your Kubernetes configuration
+
+Sonobuoy is a diagnostic tool that makes it easier to understand the state of a Kubernetes cluster by running a choice of configuration tests in an accessible and non-destructive manner.
+
+[https://sonobuoy.io/](https://sonobuoy.io/)
\ No newline at end of file
diff --git a/custom-src/frontend/assets/custom/sonobuoy.png b/custom-src/frontend/assets/custom/sonobuoy.png
new file mode 100644
index 0000000000..2fc164c2c5
Binary files /dev/null and b/custom-src/frontend/assets/custom/sonobuoy.png differ
diff --git a/custom-src/frontend/assets/custom/sonobuoy.svg b/custom-src/frontend/assets/custom/sonobuoy.svg
new file mode 100644
index 0000000000..96eee04212
--- /dev/null
+++ b/custom-src/frontend/assets/custom/sonobuoy.svg
@@ -0,0 +1,81 @@
+
+
\ No newline at end of file
diff --git a/deploy/containers/clair/Chart.yaml b/deploy/containers/clair/Chart.yaml
new file mode 100644
index 0000000000..74b02afa8d
--- /dev/null
+++ b/deploy/containers/clair/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+appVersion: 0.1.0
+description: A Helm chart for Clair
+name: clair
+version: 0.1.0
diff --git a/deploy/containers/clair/templates/_helpers.tpl b/deploy/containers/clair/templates/_helpers.tpl
new file mode 100644
index 0000000000..0b1b1831d5
--- /dev/null
+++ b/deploy/containers/clair/templates/_helpers.tpl
@@ -0,0 +1,107 @@
+{{/* vim: set filetype=mustache: */}}
+
+{{/*
+Determine external IPs:
+This will do the following:
+1. Check for Legacy SCF Config format
+2. Check for Metrics specific External IP
+3. Check for New SCF Config format
+4. Check for new Metrics External IPS
+*/}}
+{{- define "service.externalIPs" -}}
+{{- if .Values.clair.externalIP }}
+ externalIPs:
+{{- printf "\n - %s" .Values.clair.externalIP | indent 3 -}}
+{{- printf "\n" -}}
+{{- else if .Values.clair.service -}}
+{{- if .Values.clair.service.externalIPs }}
+ externalIPs:
+{{- range .Values.clair.service.externalIPs -}}
+{{ printf "\n- %s" . | indent 4 }}
+{{- end -}}
+{{- printf "\n" -}}
+{{- end -}}
+{{ end }}
+{{ end }}
+
+{{/*
+Image pull secret
+*/}}
+{{- define "imagePullSecret" }}
+{{- printf "{\"%s\":{\"username\": \"%s\",\"password\":\"%s\",\"email\":\"%s\",\"auth\": \"%s\"}}" .Values.kube.registry.hostname .Values.kube.registry.username .Values.kube.registry.password .Values.kube.registry.email (printf "%s:%s" .Values.kube.registry.username .Values.kube.registry.password | b64enc) | b64enc }}
+{{- end }}
+
+{{/*
+Service type:
+*/}}
+{{- define "service.serviceType" -}}
+{{- if .Values.clair.service -}}
+{{- default "ClusterIP" .Values.clair.service.type -}}
+{{- else -}}
+ClusterIP
+{{- end -}}
+{{- end -}}
+
+{{/*
+Service port:
+*/}}
+{{- define "service.servicePort" -}}
+{{- if .Values.clair.service -}}
+{{ default 6060 .Values.clair.service.servicePort}}
+{{- else -}}
+6060
+{{- end -}}
+{{- end -}}
+
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "metrics.certName" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Generate self-signed certificate for ingress if needed
+*/}}
+{{- define "metrics.generateIngressCertificate" -}}
+{{- $altNames := list (printf "%s" .Values.clair.service.ingress.host) (printf "%s.%s" (include "metrics.certName" .) .Release.Namespace ) ( printf "%s.%s.svc" (include "metrics.certName" .) .Release.Namespace ) -}}
+{{- $ca := genCA "stratos-ca" 365 -}}
+{{- $cert := genSignedCert ( include "metrics.certName" . ) nil $altNames 365 $ca -}}
+{{- if .Values.clair.service.ingress.tls.crt }}
+ tls.crt: {{ .Values.clair.service.ingress.tls.crt | b64enc | quote }}
+{{- else }}
+ tls.crt: {{ $cert.Cert | b64enc | quote }}
+{{- end -}}
+{{- if .Values.clair.service.ingress.tls.key }}
+ tls.key: {{ .Values.clair.service.ingress.tls.key | b64enc | quote }}
+{{- else }}
+ tls.key: {{ $cert.Key | b64enc | quote }}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Ingress Host from .Values.clair.service
+*/}}
+{{- define "ingress.host.value" -}}
+{{- if .Values.clair.service -}}
+{{- if .Values.clair.service.ingress -}}
+{{- if .Values.clair.service.ingress.host -}}
+{{ .Values.clair.service.ingress.host }}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Ingress Host:
+*/}}
+{{- define "ingress.host" -}}
+{{ $host := (include "ingress.host.value" .) }}
+{{- if $host -}}
+{{ $host | quote }}
+{{- else if .Values.env.DOMAIN -}}
+{{ print "metrics." .Values.env.DOMAIN }}
+{{- else -}}
+{{ required "Host name is required" $host | quote }}
+{{- end -}}
+{{- end -}}
diff --git a/deploy/containers/clair/templates/database.yaml b/deploy/containers/clair/templates/database.yaml
new file mode 100644
index 0000000000..e6fbc3b1e9
--- /dev/null
+++ b/deploy/containers/clair/templates/database.yaml
@@ -0,0 +1,35 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "{{ .Release.Name }}-pgdb"
+spec:
+ replicas: 1
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: 0
+ maxUnavailable: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/component: "stratos-clair-pgdb"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "stratos-clair-pgdb"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+ spec:
+ hostname: postgres
+ containers:
+ - image: "arminc/clair-db:latest"
+ imagePullPolicy: {{.Values.clair.imagePullPolicy}}
+ name: pgdb
+ ports:
+ - containerPort: 5432
+ name: postgres
+ protocol: TCP
diff --git a/deploy/containers/clair/templates/deployment.yaml b/deploy/containers/clair/templates/deployment.yaml
new file mode 100644
index 0000000000..d750d19d8d
--- /dev/null
+++ b/deploy/containers/clair/templates/deployment.yaml
@@ -0,0 +1,34 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "{{ .Release.Name }}-server"
+spec:
+ replicas: 1
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: 0
+ maxUnavailable: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/component: "stratos-clair-server"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "stratos-clair-server"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+ spec:
+ containers:
+ - image: arminc/clair-local-scan:latest
+ imagePullPolicy: {{.Values.clair.imagePullPolicy}}
+ name: server
+ ports:
+ - containerPort: 6060
+ name: api
+ protocol: TCP
diff --git a/deploy/containers/clair/templates/ingress.yaml b/deploy/containers/clair/templates/ingress.yaml
new file mode 100644
index 0000000000..d6777c5ee7
--- /dev/null
+++ b/deploy/containers/clair/templates/ingress.yaml
@@ -0,0 +1,77 @@
+{{- if .Values.clair.service -}}
+{{- if .Values.clair.service.ingress -}}
+{{- if .Values.clair.service.ingress.enabled -}}
+{{- if not .Values.clair.service.ingress.secretName -}}
+---
+# The certificate and key for the TLS secret are passed through ingress.tls.crt and ingress.tls.key
+# respectively. If the operator does not provide these values at installation time, the TLS secret
+# will contain empty values. The standard behaviour for NGINX ingress controller is to provide a
+# fake certificate instead. It is useful only for testing and development. It is expected that for
+# production use the operator will provide these values.
+apiVersion: "v1"
+kind: "Secret"
+type: kubernetes.io/tls
+metadata:
+ name: "{{ .Release.Name }}-ingress-tls"
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "metrics-ingress-tls"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+data:
+{{ template "metrics.generateIngressCertificate" . }}
+{{- end }}
+
+---
+# Ingress for the Clair service
+apiVersion: "networking.k8s.io/v1beta1"
+kind: "Ingress"
+metadata:
+ name: "{{ .Release.Name }}-ingress"
+ annotations:
+ {{- if hasKey .Values.clair.service.ingress.annotations "kubernetes.io/ingress.class" | not -}}
+ {{ $_ := set .Values.clair.service.ingress.annotations "kubernetes.io/ingress.class" "nginx" }}
+ {{- end }}
+ {{- if hasKey .Values.clair.service.ingress.annotations "kubernetes.io/ingress.allow-http" | not -}}
+ {{ $_ := set .Values.clair.service.ingress.annotations "kubernetes.io/ingress.allow-http" "false" }}
+ {{- end }}
+ {{- if hasKey .Values.clair.service.ingress.annotations "nginx.ingress.kubernetes.io/secure-backends" | not -}}
+ {{ $_ := set .Values.clair.service.ingress.annotations "nginx.ingress.kubernetes.io/secure-backends" "true" }}
+ {{- end }}
+ {{- if hasKey .Values.clair.service.ingress.annotations "nginx.ingress.kubernetes.io/backend-protocol" | not -}}
+ {{ $_ := set .Values.clair.service.ingress.annotations "nginx.ingress.kubernetes.io/backend-protocol" "HTTPS" }}
+ {{- end }}
+ {{- if hasKey .Values.clair.service.ingress.annotations "nginx.ingress.kubernetes.io/ssl-redirect" | not -}}
+ {{ $_ := set .Values.clair.service.ingress.annotations "nginx.ingress.kubernetes.io/ssl-redirect" "false" }}
+ {{- end }}
+ {{- if hasKey .Values.clair.service.ingress.annotations "nginx.ingress.kubernetes.io/proxy-body-size" | not -}}
+ {{ $_ := set .Values.clair.service.ingress.annotations "nginx.ingress.kubernetes.io/proxy-body-size" "200m" }}
+ {{- end }}
+ {{ $_ := set .Values.clair.service.ingress.annotations "nginx.org/websocket-services" (print .Release.Name "-metrics-nginx") }}
+{{ toYaml .Values.clair.service.ingress.annotations | indent 4 }}
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/component: "clair-ingress"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+{{- range $key, $value := .Values.clair.service.ingress.extraLabels }}
+ {{ $key }}: {{ $value }}
+{{- end }}
+spec:
+ tls:
+ - secretName: {{ default (print .Release.Name "-ingress-tls") .Values.clair.service.ingress.secretName | quote }}
+ hosts:
+ - {{ template "ingress.host" . }}
+ rules:
+ - host: {{ template "ingress.host" . }}
+ http:
+ paths:
+ - path: "/"
+ backend:
+ serviceName: "{{ .Release.Name }}-clair-server"
+ servicePort: 6060
+{{- end }}
+{{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/deploy/containers/clair/templates/service-db.yaml b/deploy/containers/clair/templates/service-db.yaml
new file mode 100644
index 0000000000..14912e53c6
--- /dev/null
+++ b/deploy/containers/clair/templates/service-db.yaml
@@ -0,0 +1,32 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: "postgres"
+{{- if .Values.clair.service -}}
+{{- if .Values.clair.service.annotations }}
+ annotations:
+{{ toYaml .Values.clair.service.annotations | indent 4 }}
+{{- end }}
+{{- end }}
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "clair-db-service"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+{{- if .Values.clair.service -}}
+{{- if .Values.clair.service.extraLabels }}
+{{ toYaml .Values.clair.service.extraLabels | indent 4 }}
+{{- end }}
+{{- end }}
+spec:
+ type: ClusterIP
+ ports:
+ - name: postgres
+ port: 5432
+ targetPort: 5432
+ selector:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/component: stratos-clair-pgdb
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
diff --git a/deploy/containers/clair/templates/service.yaml b/deploy/containers/clair/templates/service.yaml
new file mode 100644
index 0000000000..42dacfac76
--- /dev/null
+++ b/deploy/containers/clair/templates/service.yaml
@@ -0,0 +1,56 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+{{- if .Values.clair.service -}}
+{{- if .Values.clair.service.annotations }}
+ annotations:
+{{ toYaml .Values.clair.service.annotations | indent 4 }}
+{{- end }}
+{{- end }}
+ name: {{ .Release.Name }}-metrics-api
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "stratos-clair-service"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+spec:
+ type: {{ template "service.serviceType" . }}
+{{- if .Values.clair.service }}
+{{- if .Values.clair.service.clusterIP }}
+ clusterIP: {{ .Values.clair.service.clusterIP }}
+{{- end }}
+{{- end }}
+{{- template "service.externalIPs" . }}
+{{- if .Values.clair.service -}}
+{{- if .Values.clair.service.loadBalancerIP }}
+ loadBalancerIP: {{ .Values.clair.service.loadBalancerIP }}
+{{- end }}
+{{- end -}}
+{{- if .Values.clair.service }}
+{{- if .Values.clair.service.loadBalancerSourceRanges }}
+ loadBalancerSourceRanges:
+ {{- range $cidr := .Values.clair.service.loadBalancerSourceRanges }}
+ - {{ $cidr }}
+ {{- end }}
+{{- end }}
+{{- end }}
+{{- if .Values.clair.service -}}
+{{- if .Values.clair.service.externalName }}
+ externalName: {{ .Values.clair.service.externalName }}
+{{- end }}
+{{- end }}
+ ports:
+ - name: nginx
+ port: {{ template "service.servicePort" . }}
+ protocol: TCP
+ targetPort: 6060
+ {{- if .Values.clair.service.nodePort }}
+ nodePort: {{ .Values.clair.service.nodePort }}
+ {{- end }}
+ selector:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/component: "stratos-clair-server"
+
+# DB Service needs to be called 'postgres'
\ No newline at end of file
diff --git a/deploy/containers/clair/templates/worker.yaml b/deploy/containers/clair/templates/worker.yaml
new file mode 100644
index 0000000000..32ac73cfd3
--- /dev/null
+++ b/deploy/containers/clair/templates/worker.yaml
@@ -0,0 +1,30 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "{{ .Release.Name }}-test"
+spec:
+ replicas: 1
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: 0
+ maxUnavailable: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/component: "stratos-clair-test"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "stratos-clair-test"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+ spec:
+ containers:
+ - image: splatform/stratos-uaa:latest
+ imagePullPolicy: {{.Values.clair.imagePullPolicy}}
+ name: test
diff --git a/deploy/containers/clair/values.yaml b/deploy/containers/clair/values.yaml
new file mode 100644
index 0000000000..647dd3a6e8
--- /dev/null
+++ b/deploy/containers/clair/values.yaml
@@ -0,0 +1,26 @@
+
+clair:
+ imagePullPolicy: Always
+ service:
+ annotations: []
+ externalIPs: []
+ loadBalancerIP:
+ loadBalancerSourceRanges: []
+ servicePort: 443
+ #nodePort: 30000
+ type: ClusterIP
+
+ externalName:
+ ingress:
+ ## If true, Ingress will be created
+ enabled: false
+ ## Additional annotations
+ annotations: {}
+ ## Additional labels
+ extraLabels: {}
+ ## Host for the ingress
+ # Defaults to metrics.[env.Domain] if env.Domain is set and host is not
+ host:
+ # Name of secret containing TLS certificate
+ secretName:
+
diff --git a/deploy/kubernetes/console/templates/analyzers.yaml b/deploy/kubernetes/console/templates/analyzers.yaml
new file mode 100644
index 0000000000..9398b9ad26
--- /dev/null
+++ b/deploy/kubernetes/console/templates/analyzers.yaml
@@ -0,0 +1,68 @@
+---
+{{- if semverCompare ">=1.16" (printf "%s.%s" .Capabilities.KubeVersion.Major (trimSuffix "+" .Capabilities.KubeVersion.Minor) )}}
+apiVersion: apps/v1
+{{- else }}
+apiVersion: extensions/v1beta1
+{{- end }}
+kind: Deployment
+metadata:
+ name: stratos-analyzers
+ labels:
+ app.kubernetes.io/name: "stratos"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "stratos-analyzers"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: "stratos"
+ app.kubernetes.io/component: "stratos-analyzers"
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: "stratos"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "stratos-analyzers"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+ app: "{{ .Release.Name }}"
+ spec:
+ containers:
+ - name: chartsync
+ image: {{.Values.kube.registry.hostname}}/{{.Values.kube.organization}}/{{ .Values.images.analyzers }}:{{.Values.consoleVersion}}
+ imagePullPolicy: {{.Values.imagePullPolicy}}
+ ports:
+ - name: api
+ containerPort: 8090
+ env:
+ - name: ANALYSIS_SCRIPTS_DIR
+ value: "/scripts"
+ - name: ANALYSIS_REPORTS_DIR
+ value: "/reports"
+ - name: CLAIR_SERVER
+ {{- if not .Values.console.clairServer }}
+ value: "http://stratos-clair-api:6060"
+ {{- else }}
+ value: {{ .Values.console.clairServer | quote }}
+ {{- end }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: "{{ .Release.Name }}-analyzers"
+ labels:
+ app.kubernetes.io/name: "stratos"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "stratos-analyzers-service"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+spec:
+ type: ClusterIP
+ ports:
+ - name: analyzers
+ port: 8090
+ targetPort: 8090
+ selector:
+ app: "{{ .Release.Name }}"
+ app.kubernetes.io/component: "stratos-analyzers"
diff --git a/deploy/kubernetes/console/templates/clair/clair-database.yaml b/deploy/kubernetes/console/templates/clair/clair-database.yaml
new file mode 100644
index 0000000000..9580de8338
--- /dev/null
+++ b/deploy/kubernetes/console/templates/clair/clair-database.yaml
@@ -0,0 +1,37 @@
+{{- if not .Values.console.clairServer -}}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "stratos-clair-pgdb"
+spec:
+ replicas: 1
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: 0
+ maxUnavailable: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/component: "stratos-clair-pgdb"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "stratos-clair-pgdb"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+ spec:
+ hostname: postgres
+ containers:
+ - image: "arminc/clair-db:latest"
+ imagePullPolicy: {{.Values.imagePullPolicy}}
+ name: pgdb
+ ports:
+ - containerPort: 5432
+ name: postgres
+ protocol: TCP
+{{- end }}
\ No newline at end of file
diff --git a/deploy/kubernetes/console/templates/clair/clair-server.yaml b/deploy/kubernetes/console/templates/clair/clair-server.yaml
new file mode 100644
index 0000000000..c59c64b0c9
--- /dev/null
+++ b/deploy/kubernetes/console/templates/clair/clair-server.yaml
@@ -0,0 +1,36 @@
+{{- if not .Values.console.clairServer -}}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: "stratos-clair-server"
+spec:
+ replicas: 1
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: 0
+ maxUnavailable: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/component: "stratos-clair-server"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "stratos-clair-server"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+ spec:
+ containers:
+ - image: arminc/clair-local-scan:latest
+ imagePullPolicy: {{.Values.imagePullPolicy}}
+ name: server
+ ports:
+ - containerPort: 6060
+ name: api
+ protocol: TCP
+{{- end }}
diff --git a/deploy/kubernetes/console/templates/clair/clair-service-db.yaml b/deploy/kubernetes/console/templates/clair/clair-service-db.yaml
new file mode 100644
index 0000000000..9111031a12
--- /dev/null
+++ b/deploy/kubernetes/console/templates/clair/clair-service-db.yaml
@@ -0,0 +1,34 @@
+{{- if not .Values.console.clairServer -}}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: "postgres"
+{{- if .Values.console.service -}}
+{{- if .Values.console.service.annotations }}
+ annotations:
+{{ toYaml .Values.console.service.annotations | indent 4 }}
+{{- end }}
+{{- end }}
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "clair-db-service"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+{{- if .Values.console.service -}}
+{{- if .Values.console.service.extraLabels }}
+{{ toYaml .Values.console.service.extraLabels | indent 4 }}
+{{- end }}
+{{- end }}
+spec:
+ type: ClusterIP
+ ports:
+ - name: postgres
+ port: 5432
+ targetPort: 5432
+ selector:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/component: stratos-clair-pgdb
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+{{- end }}
diff --git a/deploy/kubernetes/console/templates/clair/clair-service-server.yaml b/deploy/kubernetes/console/templates/clair/clair-service-server.yaml
new file mode 100644
index 0000000000..ea249300c4
--- /dev/null
+++ b/deploy/kubernetes/console/templates/clair/clair-service-server.yaml
@@ -0,0 +1,30 @@
+{{- if not .Values.console.clairServer -}}
+---
+apiVersion: v1
+kind: Service
+metadata:
+{{- if .Values.console.service -}}
+{{- if .Values.console.service.annotations }}
+ annotations:
+{{ toYaml .Values.console.service.annotations | indent 4 }}
+{{- end }}
+{{- end }}
+ name: stratos-clair-api
+ labels:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
+ app.kubernetes.io/component: "stratos-clair-service"
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+spec:
+ type: ClusterIP
+ ports:
+ - name: server
+ port: 6060
+ protocol: TCP
+ targetPort: 6060
+ selector:
+ app.kubernetes.io/name: "stratos-clair"
+ app.kubernetes.io/component: "stratos-clair-server"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+{{- end }}
diff --git a/deploy/kubernetes/console/templates/deployment.yaml b/deploy/kubernetes/console/templates/deployment.yaml
index c4123af521..2c372a25db 100644
--- a/deploy/kubernetes/console/templates/deployment.yaml
+++ b/deploy/kubernetes/console/templates/deployment.yaml
@@ -273,6 +273,8 @@ spec:
value: "mongodb://{{ .Release.Name }}-fdbdoclayer:27016"
- name: SYNC_SERVER_URL
value: "http://{{ .Release.Name }}-chartsync:8080"
+ - name: ANALYSIS_SERVICES_API
+ value: "http://{{ .Release.Name }}-analyzers:8090"
readinessProbe:
httpGet:
path: /pp/v1/ping
diff --git a/deploy/kubernetes/console/values.yaml b/deploy/kubernetes/console/values.yaml
index 975a8073de..99e9487d04 100644
--- a/deploy/kubernetes/console/values.yaml
+++ b/deploy/kubernetes/console/values.yaml
@@ -57,6 +57,7 @@ console:
servicePort: 80
# nodePort: 30001
+
# Name of config map that provides the template files for user invitation emails
templatesConfigMapName:
@@ -66,6 +67,9 @@ console:
# Enable/disable Tech Preview
techPreview: false
+ # clair server - set if you are using your own Clair server
+ clairServer: ~
+
ui:
# Override the default maximum number of entities that a configured list can fetch. When a list meets this amount additional pages are not fetched
listMaxSize:
@@ -113,6 +117,7 @@ images:
fdbserver: stratos-fdbserver
fdbdoclayer: stratos-fdbdoclayer
chartsync: stratos-chartsync
+ analyzers: stratos-analyzers
# Specify which storage class should be used for PVCs
#storageClass: default
diff --git a/docs/extensions.md b/docs/extensions.md
index 72a5175213..cb32749cc4 100644
--- a/docs/extensions.md
+++ b/docs/extensions.md
@@ -161,7 +161,7 @@ First, create the custom-src folder structure - from the top-level of the Strato
```
mkdir -p custom-src/frontend/app/custom
-mkdir -p custom-src/frontend/assets/custom
+mkdir -p /frontend/assets/custom
```
Next, run the customize task:
diff --git a/package-lock.json b/package-lock.json
index e43b5be2d5..c956b01949 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2492,6 +2492,12 @@
"defer-to-connect": "^1.0.1"
}
},
+ "@tootallnate/once": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
+ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
+ "dev": true
+ },
"@tweenjs/tween.js": {
"version": "17.4.0",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-17.4.0.tgz",
@@ -2902,9 +2908,9 @@
}
},
"acorn": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
- "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
+ "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
"dev": true
},
"acorn-jsx": {
@@ -4534,16 +4540,27 @@
"dev": true
},
"codecov": {
- "version": "3.6.1",
- "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.6.1.tgz",
- "integrity": "sha512-IUJB6WG47nWK7o50etF8jBadxdMw7DmoQg05yIljstXFBGB6clOZsIj6iD4P82T2YaIU3qq+FFu8K9pxgkCJDQ==",
+ "version": "3.6.5",
+ "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.6.5.tgz",
+ "integrity": "sha512-v48WuDMUug6JXwmmfsMzhCHRnhUf8O3duqXvltaYJKrO1OekZWpB/eH6iIoaxMl8Qli0+u3OxptdsBOYiD7VAQ==",
"dev": true,
"requires": {
- "argv": "^0.0.2",
- "ignore-walk": "^3.0.1",
- "js-yaml": "^3.13.1",
- "teeny-request": "^3.11.3",
- "urlgrey": "^0.4.4"
+ "argv": "0.0.2",
+ "ignore-walk": "3.0.3",
+ "js-yaml": "3.13.1",
+ "teeny-request": "6.0.1",
+ "urlgrey": "0.4.4"
+ },
+ "dependencies": {
+ "ignore-walk": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
+ "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
+ "dev": true,
+ "requires": {
+ "minimatch": "^3.0.4"
+ }
+ }
}
},
"codelyzer": {
@@ -7488,8 +7505,8 @@
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"optional": true,
"requires": {
- "delegates": "1.0.0",
- "readable-stream": "2.3.6"
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
}
},
"balanced-match": {
@@ -7504,7 +7521,7 @@
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"optional": true,
"requires": {
- "balanced-match": "1.0.0",
+ "balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
@@ -7544,7 +7561,7 @@
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"optional": true,
"requires": {
- "ms": "2.1.1"
+ "ms": "^2.1.1"
}
},
"deep-extend": {
@@ -7571,7 +7588,7 @@
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
"optional": true,
"requires": {
- "minipass": "2.3.5"
+ "minipass": "^2.2.1"
}
},
"fs.realpath": {
@@ -7586,14 +7603,14 @@
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"optional": true,
"requires": {
- "aproba": "1.2.0",
- "console-control-strings": "1.1.0",
- "has-unicode": "2.0.1",
- "object-assign": "4.1.1",
- "signal-exit": "3.0.2",
- "string-width": "1.0.2",
- "strip-ansi": "3.0.1",
- "wide-align": "1.1.3"
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
}
},
"glob": {
@@ -7602,12 +7619,12 @@
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"optional": true,
"requires": {
- "fs.realpath": "1.0.0",
- "inflight": "1.0.6",
- "inherits": "2.0.3",
- "minimatch": "3.0.4",
- "once": "1.4.0",
- "path-is-absolute": "1.0.1"
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
}
},
"has-unicode": {
@@ -7622,7 +7639,7 @@
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"optional": true,
"requires": {
- "safer-buffer": "2.1.2"
+ "safer-buffer": ">= 2.1.2 < 3"
}
},
"ignore-walk": {
@@ -7631,7 +7648,7 @@
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"optional": true,
"requires": {
- "minimatch": "3.0.4"
+ "minimatch": "^3.0.4"
}
},
"inflight": {
@@ -7640,8 +7657,8 @@
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"optional": true,
"requires": {
- "once": "1.4.0",
- "wrappy": "1.0.2"
+ "once": "^1.3.0",
+ "wrappy": "1"
}
},
"inherits": {
@@ -7662,7 +7679,7 @@
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": {
- "number-is-nan": "1.0.1"
+ "number-is-nan": "^1.0.0"
}
},
"isarray": {
@@ -7677,7 +7694,7 @@
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"requires": {
- "brace-expansion": "1.1.11"
+ "brace-expansion": "^1.1.7"
}
},
"minimist": {
@@ -7692,8 +7709,8 @@
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"optional": true,
"requires": {
- "safe-buffer": "5.1.2",
- "yallist": "3.0.3"
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
}
},
"minizlib": {
@@ -7702,7 +7719,7 @@
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
"optional": true,
"requires": {
- "minipass": "2.3.5"
+ "minipass": "^2.2.1"
}
},
"mkdirp": {
@@ -7726,9 +7743,9 @@
"integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
"optional": true,
"requires": {
- "debug": "4.1.1",
- "iconv-lite": "0.4.24",
- "sax": "1.2.4"
+ "debug": "^4.1.0",
+ "iconv-lite": "^0.4.4",
+ "sax": "^1.2.4"
}
},
"node-pre-gyp": {
@@ -7737,16 +7754,16 @@
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
"optional": true,
"requires": {
- "detect-libc": "1.0.3",
- "mkdirp": "0.5.1",
- "needle": "2.3.0",
- "nopt": "4.0.1",
- "npm-packlist": "1.4.1",
- "npmlog": "4.1.2",
- "rc": "1.2.8",
- "rimraf": "2.6.3",
- "semver": "5.7.0",
- "tar": "4.4.8"
+ "detect-libc": "^1.0.2",
+ "mkdirp": "^0.5.1",
+ "needle": "^2.2.1",
+ "nopt": "^4.0.1",
+ "npm-packlist": "^1.1.6",
+ "npmlog": "^4.0.2",
+ "rc": "^1.2.7",
+ "rimraf": "^2.6.1",
+ "semver": "^5.3.0",
+ "tar": "^4"
}
},
"nopt": {
@@ -7755,8 +7772,8 @@
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"optional": true,
"requires": {
- "abbrev": "1.1.1",
- "osenv": "0.1.5"
+ "abbrev": "1",
+ "osenv": "^0.1.4"
}
},
"npm-bundled": {
@@ -7771,8 +7788,8 @@
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
"optional": true,
"requires": {
- "ignore-walk": "3.0.1",
- "npm-bundled": "1.0.6"
+ "ignore-walk": "^3.0.1",
+ "npm-bundled": "^1.0.1"
}
},
"npmlog": {
@@ -7781,10 +7798,10 @@
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"optional": true,
"requires": {
- "are-we-there-yet": "1.1.5",
- "console-control-strings": "1.1.0",
- "gauge": "2.7.4",
- "set-blocking": "2.0.0"
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
}
},
"number-is-nan": {
@@ -7805,7 +7822,7 @@
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"optional": true,
"requires": {
- "wrappy": "1.0.2"
+ "wrappy": "1"
}
},
"os-homedir": {
@@ -7826,8 +7843,8 @@
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"optional": true,
"requires": {
- "os-homedir": "1.0.2",
- "os-tmpdir": "1.0.2"
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
}
},
"path-is-absolute": {
@@ -7848,10 +7865,10 @@
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"optional": true,
"requires": {
- "deep-extend": "0.6.0",
- "ini": "1.3.5",
- "minimist": "1.2.0",
- "strip-json-comments": "2.0.1"
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
},
"dependencies": {
"minimist": {
@@ -7868,13 +7885,13 @@
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"optional": true,
"requires": {
- "core-util-is": "1.0.2",
- "inherits": "2.0.3",
- "isarray": "1.0.0",
- "process-nextick-args": "2.0.0",
- "safe-buffer": "5.1.2",
- "string_decoder": "1.1.1",
- "util-deprecate": "1.0.2"
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
}
},
"rimraf": {
@@ -7883,7 +7900,7 @@
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"optional": true,
"requires": {
- "glob": "7.1.3"
+ "glob": "^7.1.3"
}
},
"safe-buffer": {
@@ -7927,9 +7944,9 @@
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": {
- "code-point-at": "1.1.0",
- "is-fullwidth-code-point": "1.0.0",
- "strip-ansi": "3.0.1"
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
}
},
"string_decoder": {
@@ -7938,7 +7955,7 @@
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"optional": true,
"requires": {
- "safe-buffer": "5.1.2"
+ "safe-buffer": "~5.1.0"
}
},
"strip-ansi": {
@@ -7946,7 +7963,7 @@
"bundled": true,
"optional": true,
"requires": {
- "ansi-regex": "2.1.1"
+ "ansi-regex": "^2.0.0"
}
},
"strip-json-comments": {
@@ -7961,13 +7978,13 @@
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
"optional": true,
"requires": {
- "chownr": "1.1.1",
- "fs-minipass": "1.2.5",
- "minipass": "2.3.5",
- "minizlib": "1.2.1",
- "mkdirp": "0.5.1",
- "safe-buffer": "5.1.2",
- "yallist": "3.0.3"
+ "chownr": "^1.1.1",
+ "fs-minipass": "^1.2.5",
+ "minipass": "^2.3.4",
+ "minizlib": "^1.1.1",
+ "mkdirp": "^0.5.0",
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.2"
}
},
"util-deprecate": {
@@ -7982,7 +7999,7 @@
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"optional": true,
"requires": {
- "string-width": "1.0.2"
+ "string-width": "^1.0.2 || 2"
}
},
"wrappy": {
@@ -10291,9 +10308,9 @@
"dev": true
},
"kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
},
"klaw-sync": {
"version": "2.1.0",
@@ -14794,6 +14811,15 @@
"stream-shift": "^1.0.0"
}
},
+ "stream-events": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
+ "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
+ "dev": true,
+ "requires": {
+ "stubs": "^3.0.0"
+ }
+ },
"stream-exhaust": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz",
@@ -14933,6 +14959,12 @@
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true
},
+ "stubs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
+ "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=",
+ "dev": true
+ },
"style-loader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.0.0.tgz",
@@ -15162,14 +15194,71 @@
}
},
"teeny-request": {
- "version": "3.11.3",
- "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz",
- "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz",
+ "integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==",
"dev": true,
"requires": {
- "https-proxy-agent": "^2.2.1",
+ "http-proxy-agent": "^4.0.0",
+ "https-proxy-agent": "^4.0.0",
"node-fetch": "^2.2.0",
+ "stream-events": "^1.0.5",
"uuid": "^3.3.2"
+ },
+ "dependencies": {
+ "agent-base": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz",
+ "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==",
+ "dev": true,
+ "requires": {
+ "debug": "4"
+ }
+ },
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "http-proxy-agent": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
+ "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
+ "dev": true,
+ "requires": {
+ "@tootallnate/once": "1",
+ "agent-base": "6",
+ "debug": "4"
+ }
+ },
+ "https-proxy-agent": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz",
+ "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==",
+ "dev": true,
+ "requires": {
+ "agent-base": "5",
+ "debug": "4"
+ },
+ "dependencies": {
+ "agent-base": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
+ "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==",
+ "dev": true
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ }
}
},
"temp": {
diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts
index c0b9a3e987..a8e0c4efc4 100644
--- a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts
+++ b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts
@@ -96,7 +96,7 @@ export class ApplicationTabsBaseComponent implements OnInit, OnDestroy {
switchMap(space => this.currentUserPermissionsService.can(CurrentUserPermissions.APPLICATION_VIEW_ENV_VARS,
this.applicationService.cfGuid, space.metadata.guid)
),
- map(can => !can)
+ map(can => !can),
);
this.tabLinks = [
diff --git a/src/frontend/packages/core/sass/_all-theme.scss b/src/frontend/packages/core/sass/_all-theme.scss
index 3daaa41180..779deda156 100644
--- a/src/frontend/packages/core/sass/_all-theme.scss
+++ b/src/frontend/packages/core/sass/_all-theme.scss
@@ -66,6 +66,7 @@
@import '../../cloud-foundry/src/features/service-catalog/service-catalog-page/service-catalog-page.component.theme';
@import '../../cloud-foundry/src/features/applications/application-wall/application-wall.component.theme';
@import '../../core/src/features/error-page/error-page/error-page.component.theme.scss';
+@import '../../core/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.theme';
// Defaults
$side-nav-light-text: #fff;
@@ -159,6 +160,7 @@ $side-nav-light-active: #484848;
@include code-block-theme($theme, $app-theme);
@include copy-to-clipboard-theme($theme, $app-theme);
@include app-user-avatar-theme($theme, $app-theme);
+ @include kube-analysis-report-theme($theme, $app-theme);
}
@function app-generate-nav-theme($theme, $nav-theme: null) {
@@ -177,6 +179,6 @@ $side-nav-light-active: #484848;
$warn: map-get($theme, warn);
$primary: map-get($theme, primary);
$white: #fff; // Use default palette for status
- @return (success: map-get($mat-green, 500), warning: map-get($mat-orange, 500), danger: mat-color($warn), tentative: map-get($mat-grey, 500), busy: mat-color($primary), text: $white, );
+ @return (success: map-get($mat-green, 500), info: map-get($mat-blue, 500), warning: map-get($mat-orange, 500), danger: mat-color($warn), tentative: map-get($mat-grey, 500), busy: mat-color($primary), text: $white, );
}
}
diff --git a/src/frontend/packages/core/sass/components/text-status.theme.scss b/src/frontend/packages/core/sass/components/text-status.theme.scss
index 68b3d1821b..a054375175 100644
--- a/src/frontend/packages/core/sass/components/text-status.theme.scss
+++ b/src/frontend/packages/core/sass/components/text-status.theme.scss
@@ -6,6 +6,7 @@
$status-warning: map-get($status-colors, warning);
$status-danger: map-get($status-colors, danger);
$status-tentative: map-get($status-colors, tentative);
+ $status-info: map-get($status-colors, info);
.text-success {
color: $status-success;
@@ -23,6 +24,10 @@
color: $status-tentative;
}
+ .text-info {
+ color: $status-info;
+ }
+
// Border colors
.border-success {
@@ -41,4 +46,8 @@
border-color: $status-tentative;
}
+ .border-info {
+ border-color: $status-info;
+ }
+
}
diff --git a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts
index 91e4819fba..476ee57bbb 100644
--- a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts
+++ b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts
@@ -25,6 +25,7 @@ import { TabNavService } from '../../../../tab-nav.service';
import { CustomizationService } from '../../../core/customizations.types';
import { EndpointsService } from '../../../core/endpoints.service';
import { SidePanelService } from '../../../shared/services/side-panel.service';
+import { IPageSideNavTab } from '../page-side-nav/page-side-nav.component';
import { PageHeaderService } from './../../../core/page-header-service/page-header.service';
import { SideNavItem } from './../side-nav/side-nav.component';
@@ -37,7 +38,7 @@ import { SideNavItem } from './../side-nav/side-nav.component';
export class DashboardBaseComponent implements OnInit, OnDestroy, AfterViewInit {
public activeTabLabel$: Observable;
- public subNavData$: Observable<[string, Portal]>;
+ public subNavData$: Observable<[string, Portal, IPageSideNavTab]>;
public isMobile$: Observable;
public sideNavMode$: Observable;
public sideNavMode: string;
@@ -141,7 +142,8 @@ export class DashboardBaseComponent implements OnInit, OnDestroy, AfterViewInit
startWith(null)
),
this.tabNavService.tabSubNav$
- );
+ ).pipe(map(([tabNav, tabSubNav]) => [tabNav ? tabNav.label : null, tabSubNav, tabNav]));
+
// TODO: Move cf code out to cf module #3849
this.endpointsService.registerHealthCheck(
new EndpointHealthCheck(CF_ENDPOINT_TYPE, (endpoint) => {
diff --git a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts
index a91f4bd200..362fa5443f 100644
--- a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts
+++ b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts
@@ -9,6 +9,7 @@ import { TabNavService } from '../../../../tab-nav.service';
import { EntityServiceFactory } from '../../../../../store/src/entity-service-factory.service';
import { StratosTabMetadata } from '../../../core/extension/extension-service';
import { IBreadcrumb } from '../../../shared/components/breadcrumbs/breadcrumbs.types';
+import { map } from 'rxjs/operators';
export interface IPageSideNavTab extends StratosTabMetadata {
hidden$?: Observable;
@@ -50,7 +51,7 @@ export class PageSideNavComponent implements OnInit {
}
ngOnInit() {
- this.activeTab$ = this.tabNavService.getCurrentTabHeaderObservable();
+ this.activeTab$ = this.tabNavService.getCurrentTabHeaderObservable().pipe(map(item => item ? item.label : null));
}
}
diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html
index ea0067edbc..b0747565c1 100644
--- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html
+++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html
@@ -14,6 +14,20 @@
{{ labelSingular && value === '1' ? labelSingular : label }}
+
+
+ info
+ {{ alertInfo.info }}
+
+
+
warning
+
{{ alertInfo.warning }}
+
+
+ error
+ {{ alertInfo.error }}
+
+
diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss
index 482e94a189..78aa3599eb 100644
--- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss
+++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss
@@ -40,4 +40,26 @@
&__limit {
font-size: 18px;
}
+ &__alerts {
+ cursor: pointer;
+ display: flex;
+ flex: 0;
+ flex-direction: column;
+ }
+ &__alert-badge {
+ align-items: center;
+ border-radius: 4px;
+ color: #fff;
+ display: flex;
+ font-size: 14px;
+ margin-bottom: 2px;
+ padding: 2px 4px;
+
+ &> mat-icon {
+ font-size: 16px;
+ height: 16px;
+ margin-right: 2px;
+ width: 16x;
+ }
+ }
}
diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss
index eba218e2e2..f972366d35 100644
--- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss
+++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss
@@ -9,4 +9,16 @@
color: $subdued;
}
}
+
+ .number-metric-card__alert-badge {
+ &-error {
+ background-color: red;
+ }
+ &-info {
+ background-color: #1ba5da;
+ }
+ &-warning {
+ background-color: orange;
+ }
+ }
}
diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts
index e347f07b14..9dc6f1cbaa 100644
--- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts
+++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input, OnChanges, OnInit } from '@angular/core';
+import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { Store } from '@ngrx/store';
import { BehaviorSubject } from 'rxjs';
@@ -8,6 +8,14 @@ import { UtilsService } from '../../../../core/utils.service';
import { StratosStatus } from '../../../shared.types';
import { determineCardStatus } from '../card-status/card-status.component';
+enum AlertLevel {
+ OK = 0,
+ Info,
+ Warning,
+ Error,
+ Unknown,
+}
+
@Component({
selector: 'app-card-number-metric',
templateUrl: './card-number-metric.component.html',
@@ -26,6 +34,16 @@ export class CardNumberMetricComponent implements OnInit, OnChanges {
@Input() textOnly = false;
@Input() labelAtTop = false;
@Input() link: () => void | string;
+ @Output() showAlerts = new EventEmitter();
+
+ @Input('alerts')
+ set alerts(alerts) {
+ if (alerts) {
+ this.processAlerts(alerts);
+ }
+ }
+
+ alertInfo: any;
formattedValue: string;
formattedLimit: string;
@@ -102,4 +120,31 @@ export class CardNumberMetricComponent implements OnInit, OnChanges {
this.link();
}
}
+
+ processAlerts(alerts) {
+ this.alertInfo = {
+ info: 0,
+ warning: 0,
+ error: 0
+ };
+
+ alerts.forEach((alert) => {
+ switch (alert.level as AlertLevel) {
+ case AlertLevel.Warning:
+ this.alertInfo.warning++;
+ break;
+ case AlertLevel.Error:
+ this.alertInfo.error++;
+ break;
+ case AlertLevel.Info:
+ this.alertInfo.info++;
+ break;
+ }
+ });
+ }
+
+ public alertsClicked() {
+ this.showAlerts.emit(this.alertInfo);
+ }
+
}
diff --git a/src/frontend/packages/core/tab-nav.service.ts b/src/frontend/packages/core/tab-nav.service.ts
index fbdad8cad7..f272958344 100644
--- a/src/frontend/packages/core/tab-nav.service.ts
+++ b/src/frontend/packages/core/tab-nav.service.ts
@@ -63,7 +63,7 @@ export class TabNavService {
);
}
- public getCurrentTabHeader = (tabs: IPageSideNavTab[]) => {
+ private getCurrentTabHeader = (tabs: IPageSideNavTab[]) => {
if (!tabs) {
return null;
}
@@ -74,7 +74,7 @@ export class TabNavService {
if (!activeTab) {
return null;
}
- return activeTab.label;
+ return activeTab;
}
private observeSubject(subject: Subject) {
diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts
index ef05eefaf2..0a5208b715 100644
--- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts
+++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts
@@ -1,6 +1,6 @@
+import { CreatePagination } from '../../actions/pagination.actions';
import { entityCatalog } from '../../entity-catalog/entity-catalog.service';
import { EntityCatalogEntityConfig } from '../../entity-catalog/entity-catalog.types';
-import { CreatePagination } from '../../actions/pagination.actions';
import { PaginationEntityState, PaginationState } from '../../types/pagination.types';
import { spreadClientPagination } from './pagination-reducer.helper';
diff --git a/src/jetstream/default.config.properties b/src/jetstream/default.config.properties
index c2856b2cb1..46aca1b30f 100644
--- a/src/jetstream/default.config.properties
+++ b/src/jetstream/default.config.properties
@@ -72,4 +72,7 @@ INVITE_USER_CLIENT_SECRET=
#SYNC_SERVER_URL=http://127.0.0.1:8080"
# Simplify development with FDB (value is port of FDB server: 27016 for FDB, 27017 for MongoDB)
-#FDB_LOCAL_DEV=27017
\ No newline at end of file
+#FDB_LOCAL_DEV=27017
+
+# Analysis services API
+#ANALYSIS_SERVICES_API=
\ No newline at end of file
diff --git a/src/jetstream/go.mod b/src/jetstream/go.mod
index 53e96fbe80..3ab1e4464e 100644
--- a/src/jetstream/go.mod
+++ b/src/jetstream/go.mod
@@ -36,6 +36,7 @@ require (
github.com/fatih/color v1.7.0 // indirect
github.com/go-sql-driver/mysql v1.5.0
github.com/google/go-querystring v1.0.0 // indirect
+ github.com/google/martian v2.1.0+incompatible
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e // indirect
github.com/gorilla/context v1.1.1
github.com/gorilla/securecookie v1.1.1
diff --git a/src/jetstream/go.sum b/src/jetstream/go.sum
index 9bf918f27d..6bd33f6a3b 100644
--- a/src/jetstream/go.sum
+++ b/src/jetstream/go.sum
@@ -56,6 +56,7 @@ github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiU
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA=
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
@@ -130,10 +131,13 @@ github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMX
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M=
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.15+incompatible h1:+9RjdC18gMxNQVvSiXvObLu29mOFmkgdsB4cRTlV+EE=
github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea h1:n2Ltr3SrfQlf/9nOna1DoGKxLx3qTSI8Ttl6Xrqp6mw=
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cppforlife/go-patch v0.2.0 h1:Y14MnCQjDlbw7WXT4k+u6DPAA9XnygN4BfrSpI/19RU=
github.com/cppforlife/go-patch v0.2.0/go.mod h1:67a7aIi94FHDZdoeGSJRRFDp66l9MhaAG1yGxpUoFD8=
@@ -354,6 +358,7 @@ github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:Fecb
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q=
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
@@ -475,6 +480,7 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
@@ -506,6 +512,7 @@ github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJ
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830 h1:yvQ/2Pupw60ON8TYEIGGTAI77yZsWYkiOeHFZWkwlCk=
github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@@ -631,8 +638,11 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK
go.mongodb.org/mongo-driver v1.1.3 h1:++7u8r9adKhGR+I79NfEtYrk2ktjenErXM99PSufIoI=
go.mongodb.org/mongo-driver v1.1.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569 h1:nSQar3Y0E3VQF/VdZ8PTAilaXpER+d7ypdABCrpwMdg=
go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df h1:shvkWr0NAZkg4nPuE3XrKP0VuBPijjk3TfX6Y6acFNg=
go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15 h1:Z2sc4+v0JHV6Mn4kX1f2a5nruNjmV+Th32sugE8zwz8=
go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -775,6 +785,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/square/go-jose.v1 v1.1.2 h1:/5jmADZB+RiKtZGr4HxsEFOEfbfsjTKsVnqpThUpE30=
gopkg.in/square/go-jose.v1 v1.1.2/go.mod h1:QpYS+a4WhS+DTlyQIi6Ka7MS3SuR9a055rgXNEe6EiA=
@@ -814,12 +825,14 @@ k8s.io/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-201910010437
k8s.io/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20191001043732-d647ddbd755f/go.mod h1:f1tFT2pOqPzfckbG1GjHIzy3G+T2LW7rchcruNoLaiM=
k8s.io/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20191001043732-d647ddbd755f h1:X3br+JCtf40mnzQsKAnHnezd1CvCENgG5uLJTbAspZ4=
k8s.io/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20191001043732-d647ddbd755f/go.mod h1:PNw+FbGH4/s3zK9V3rAeMiHTbQz2CU/yqAkfQ2UgLVs=
+k8s.io/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20191001043732-d647ddbd755f h1:QIhu1g7jmiv/90qGiPiCOTHFYEcrL0HA5P/6G/pt7zM=
k8s.io/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20191001043732-d647ddbd755f/go.mod h1:WmFoxjELD2xtWb77Yj9RPibT5ACkQYEW9lPQtNkGtbE=
k8s.io/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20191001043732-d647ddbd755f h1:6CkT409OUoX4ZiP++1N3id3PCcOoktBvclNsDKPKrfc=
k8s.io/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20191001043732-d647ddbd755f/go.mod h1:nBogvbgjMgo7AeVA6CuqVO13LVIfmlQ11t6xzAJdBN8=
k8s.io/kubernetes/staging/src/k8s.io/client-go v0.0.0-20191001043732-d647ddbd755f h1:ksJC2cpBqkCP8bzmfDYXr65JRpt9JmANvaKIR3qggt4=
k8s.io/kubernetes/staging/src/k8s.io/client-go v0.0.0-20191001043732-d647ddbd755f/go.mod h1:GiGfbsjtP4tOW6zgpL8/vCUoyXAV5+9X2onLursPi08=
k8s.io/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20191001043732-d647ddbd755f/go.mod h1:L8deZCu6NpzgKzY91TOGKJ1JtAoHd8WyJ/HdoxqZCGo=
+k8s.io/kubernetes/staging/src/k8s.io/component-base v0.0.0-20191001043732-d647ddbd755f h1:fwZSUxpQ99UBEkIhHbzY2pE3SPU9Zn4yZkMSolEt6Jw=
k8s.io/kubernetes/staging/src/k8s.io/component-base v0.0.0-20191001043732-d647ddbd755f/go.mod h1:spPP+vRNS8EsnNNIhFCZTTuRO3XhV1WoF18HJySoZn8=
k8s.io/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20191001043732-d647ddbd755f h1:vH4+rTRLDI8z9dQCZ6cJcIi3RMGZ6JwJWyLbrSNHBCE=
k8s.io/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20191001043732-d647ddbd755f/go.mod h1:ellVfoCz8MlDjTnkqsTkU5svJOIjcK3XNx/onmixgDk=
@@ -837,6 +850,7 @@ rsc.io/letsencrypt v0.0.1/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY=
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
+sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca h1:6dsH6AYQWbyZmtttJNe8Gq1cXOeS1BdV3eW37zHilAQ=
sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
diff --git a/src/jetstream/load_plugins.go b/src/jetstream/load_plugins.go
index c97c4ce2a9..b77af4d910 100644
--- a/src/jetstream/load_plugins.go
+++ b/src/jetstream/load_plugins.go
@@ -6,12 +6,14 @@ import (
"github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/cfappssh"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/cloudfoundry"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/cloudfoundryhosting"
+ "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/kubernetes"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/metrics"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/monocular"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/userfavorites"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/userinfo"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/userinvite"
+
"github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces"
log "github.com/sirupsen/logrus"
)
@@ -38,6 +40,7 @@ func (pp *portalProxy) loadPlugins() {
{"monocular", monocular.Init},
{"userfavorites", userfavorites.Init},
{"autoscaler", autoscaler.Init},
+ {"analysis", analysis.Init},
} {
plugin, err := p.Init(pp)
pp.Plugins[p.Name] = plugin
diff --git a/src/jetstream/plugins/analysis/20200210105400_Analysis.go b/src/jetstream/plugins/analysis/20200210105400_Analysis.go
new file mode 100644
index 0000000000..f27d6bf873
--- /dev/null
+++ b/src/jetstream/plugins/analysis/20200210105400_Analysis.go
@@ -0,0 +1,43 @@
+package analysis
+
+import (
+ "database/sql"
+
+ "bitbucket.org/liamstask/goose/lib/goose"
+
+ "github.com/cloudfoundry-incubator/stratos/src/jetstream/datastore"
+)
+
+func init() {
+ datastore.RegisterMigration(20200210105400, "Analysis", func(txn *sql.Tx, conf *goose.DBConf) error {
+
+ createAnalysisTabls := "CREATE TABLE IF NOT EXISTS analysis ("
+ createAnalysisTabls += "id VARCHAR(255) NOT NULL,"
+ createAnalysisTabls += "endpoint VARCHAR(36) NOT NULL,"
+ createAnalysisTabls += "endpoint_type VARCHAR(36) NOT NULL,"
+ createAnalysisTabls += "name VARCHAR(255) NOT NULL,"
+ createAnalysisTabls += "user VARCHAR(36) NOT NULL,"
+ createAnalysisTabls += "path VARCHAR(255) NOT NULL,"
+ createAnalysisTabls += "type VARCHAR(64) NOT NULL,"
+ createAnalysisTabls += "format VARCHAR(64) NOT NULL,"
+ createAnalysisTabls += "created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,"
+ createAnalysisTabls += "acknowledged BOOLEAN NOT NULL DEFAULT FALSE,"
+ createAnalysisTabls += "status VARCHAR(16) NOT NULL,"
+ createAnalysisTabls += "duration INT NOT NULL DEFAULT 0,"
+ createAnalysisTabls += "result VARCHAR(255) NOT NULL,"
+ createAnalysisTabls += "PRIMARY KEY (id) );"
+
+ _, err := txn.Exec(createAnalysisTabls)
+ if err != nil {
+ return err
+ }
+
+ // createIndex := "CREATE INDEX charts_id ON charts (id);"
+ // _, err = txn.Exec(createIndex)
+ // if err != nil {
+ // return err
+ // }
+
+ return nil
+ })
+}
diff --git a/src/jetstream/plugins/analysis/container/Dockerfile b/src/jetstream/plugins/analysis/container/Dockerfile
new file mode 100644
index 0000000000..b2bd88d46b
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/Dockerfile
@@ -0,0 +1,61 @@
+FROM splatform/stratos-bk-build-base:leap15_1 as builder
+
+# Build the API Server for the analysis engines
+
+RUN mkdir -p /home/stratos/go/src
+WORKDIR /home/stratos/go/src
+COPY --chown=stratos:users . /home/stratos/go/src
+ARG VERSION=1.0.0
+RUN GO111MODULE=on go build -o stratos-analyzers -ldflags -X=main.appVersion=${VERSION}
+
+# Download the Analysis tools
+WORKDIR /home/stratos/analysis
+WORKDIR /home/stratos/tmp
+USER root
+
+# Analyzers ====================================================================================================================
+
+
+# Popeye
+ARG POPEYE_VERSION=0.6.2
+# Download archive - popeye executable is in main dir - move it to the analysis folder
+RUN wget https://github.com/derailed/popeye/releases/download/v${POPEYE_VERSION}/popeye_${POPEYE_VERSION}_Linux_x86_64.tar.gz && \
+ tar -xvf popeye_${POPEYE_VERSION}_Linux_x86_64.tar.gz && \
+ mv popeye ../analysis
+
+# Kube-score
+ARG KUBESCORE_VERSION=1.5.0
+RUN wget https://github.com/zegl/kube-score/releases/download/v${KUBESCORE_VERSION}/kube-score_${KUBESCORE_VERSION}_linux_amd64.tar.gz && \
+ tar -xvf kube-score_${KUBESCORE_VERSION}_linux_amd64.tar.gz && \
+ mv kube-score ../analysis
+
+# Sonobuoy
+ARG SONOBUOY_VERSION=0.17.2
+RUN wget https://github.com/vmware-tanzu/sonobuoy/releases/download/v${SONOBUOY_VERSION}/sonobuoy_${SONOBUOY_VERSION}_linux_amd64.tar.gz && \
+ tar -xvf sonobuoy_${SONOBUOY_VERSION}_linux_amd64.tar.gz && \
+ mv sonobuoy ../analysis
+
+# Need kubectl for Kubescore - TODO: Use correct version depending on cluster
+ARG KUBECTL_VERSION=1.16.2
+RUN wget https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \
+ chmod +x kubectl && \
+ mv kubectl ../analysis
+
+# klar
+ARG KLAR_VERSION=2.4.0
+RUN wget https://github.com/optiopay/klar/releases/download/v${KLAR_VERSION}/klar-${KLAR_VERSION}-linux-amd64 && \
+ mv klar-${KLAR_VERSION}-linux-amd64 klar && \
+ chmod +x klar && \
+ mv klar ../analysis
+
+# Final Container =============================================================================================================
+
+FROM splatform/stratos-bk-base:leap15_1
+
+# Copy tools to the /usr/bin folder so that they are in the path
+COPY --from=builder /home/stratos/analysis /usr/bin
+COPY --from=builder /home/stratos/go/src/stratos-analyzers /stratos-analyzers
+COPY ./scripts /scripts
+RUN mkdir /reports
+
+CMD ["/stratos-analyzers"]
diff --git a/src/jetstream/plugins/analysis/container/clair.go b/src/jetstream/plugins/analysis/container/clair.go
new file mode 100644
index 0000000000..4ebf3e2473
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/clair.go
@@ -0,0 +1,182 @@
+package main
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+)
+
+const (
+ imagesFile = "images.txt"
+)
+
+type klarImage struct {
+ Name string `json:"name"`
+ ResultFile string `json:"details"`
+ Error bool `json:"error"`
+ LayerCount int `json:"layerCount"`
+ Vulnerabilities map[string]int `json:"Vulnerabilities"`
+}
+
+type klarResult struct {
+ Images []klarImage `json:"images"`
+}
+
+type klarReport struct {
+ LayerCount int `json:"LayerCount"`
+ Vulnerabilities map[string][]interface{} `json:"Vulnerabilities"`
+}
+
+func runClair(job *AnalysisJob) error {
+
+ log.Debug("Running Clair job")
+
+ job.Busy = true
+ job.Type = "clair"
+ job.Format = "clair"
+ setJobNameAndPath(job, "Clair")
+
+ scriptPath := filepath.Join(getScriptFolder(), "clair-runner.sh")
+ args := []string{scriptPath, job.KuebConfigPath, job.Config.Namespace}
+ log.Error(scriptPath)
+
+ go func() {
+ // Use our custom script which is a wrapper around kubescore
+ cmd := exec.Command("bash", args...)
+ cmd.Dir = job.Folder
+
+ // Inherit parent environment
+ cmd.Env = os.Environ()
+
+ //cmd.Env = make([]string, 0)
+ cmd.Env = append(cmd.Env, fmt.Sprintf("KUBECONFIG=%s", job.KuebConfigPath))
+
+ start := time.Now()
+ out, err := cmd.CombinedOutput()
+ end := time.Now()
+
+ log.Info("Completed running clar")
+ log.Info(err)
+
+ // Remove any config files when done
+
+ // TODO: THIS MUST GO BACK IN
+ //job.RemoveTempFiles()
+
+ job.Duration = int(end.Sub(start).Seconds())
+
+ if err != nil {
+ // There was an error
+ // Remove the folder
+ os.Remove(job.Folder)
+ log.Error(">>>>>>>>> ERROR <<<<<<<<<")
+ log.Error(string(out))
+ log.Error(err)
+ job.Status = "error"
+ } else {
+ reportFile := filepath.Join(job.Folder, "report.log")
+ ioutil.WriteFile(reportFile, out, os.ModePerm)
+ job.Status = "completed"
+
+ err := klarProcess(job.Folder)
+ if err != nil {
+ job.Status = "error"
+ }
+ }
+ }()
+
+ return nil
+}
+
+func klarProcess(folder string) error {
+
+ // Need there to be an index file
+ imagesFilePath := filepath.Join(folder, imagesFile)
+ log.Info(imagesFilePath)
+
+ _, err := os.Stat(imagesFilePath)
+ if os.IsNotExist(err) {
+ log.Warn("File does not exist")
+ return err
+ }
+
+ // Read it
+ file, err := os.Open(imagesFilePath)
+ if err != nil {
+ log.Warn("Could not open file")
+ return err
+ }
+ defer file.Close()
+
+ result := klarResult{}
+ result.Images = make([]klarImage, 0)
+
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ name := scanner.Text()
+ if len(name) > 0 {
+ image := klarImage{}
+ image.Name = scanner.Text()
+ image.Vulnerabilities = make(map[string]int)
+
+ klarProcessImage(folder, &image)
+ result.Images = append(result.Images, image)
+ }
+ }
+
+ // Write the report json
+ data, err := json.Marshal(result)
+ if err != nil {
+ return err
+ }
+
+ reportFile := filepath.Join(folder, "report.json")
+ ioutil.WriteFile(reportFile, data, os.ModePerm)
+
+ return nil
+}
+
+func klarProcessImage(folder string, image *klarImage) {
+
+ // Check for the log file
+ logName := strings.ReplaceAll(image.Name, "/", "_")
+ logName = strings.ReplaceAll(logName, ":", "_")
+ image.ResultFile = logName + ".json"
+
+ // No log file means an error
+ logFile := filepath.Join(folder, image.ResultFile)
+ info, err := os.Stat(logFile)
+ image.Error = os.IsNotExist(err)
+
+ if !image.Error {
+ // Also an error if the file size if 0
+ image.Error = info.Size() == 0
+ if !image.Error {
+ // Read the file so we can get a summary
+ data, err := ioutil.ReadFile(logFile)
+ if err != nil {
+ image.Error = true
+ } else {
+ report := klarReport{}
+ err = json.Unmarshal(data, &report)
+ if err == nil {
+ image.LayerCount = report.LayerCount
+
+ // Get the counts for each Vulnerabiltity
+ for severity := range report.Vulnerabilities {
+ total := len(report.Vulnerabilities[severity])
+ image.Vulnerabilities[severity] = total
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/jetstream/plugins/analysis/container/go.mod b/src/jetstream/plugins/analysis/container/go.mod
new file mode 100644
index 0000000000..d9101c6c6c
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/go.mod
@@ -0,0 +1,12 @@
+module analyzers
+
+go 1.13
+
+require (
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
+ github.com/labstack/echo v3.3.10+incompatible
+ github.com/labstack/gommon v0.3.0 // indirect
+ github.com/sirupsen/logrus v1.4.2
+ github.com/valyala/fasttemplate v1.1.0 // indirect
+ golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d // indirect
+)
diff --git a/src/jetstream/plugins/analysis/container/go.sum b/src/jetstream/plugins/analysis/container/go.sum
new file mode 100644
index 0000000000..ae076adf3a
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/go.sum
@@ -0,0 +1,48 @@
+github.com/cloudfoundry-incubator/stratos v2.0.0-beta-001+incompatible h1:UUxNbLjhv2cfymub5yNN1tjjqYkteHBBagb4jcbXEIQ=
+github.com/cloudfoundry-incubator/stratos/src/jetstream v0.0.0-20200222120421-390cf0f6670b h1:52Py09Cmdnyxr750Tj5InffbWJpCDTWie0RCbxxoUAA=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
+github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
+github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
+github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
+github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
+github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
+golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/src/jetstream/plugins/analysis/container/kubescore.go b/src/jetstream/plugins/analysis/container/kubescore.go
new file mode 100644
index 0000000000..3b65ba0ffa
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/kubescore.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+)
+
+func runKubeScore(job *AnalysisJob) error {
+
+ log.Debug("Running kube-score job")
+
+ job.Busy = true
+ job.Type = "kubescore"
+ job.Format = "kubescore"
+ setJobNameAndPath(job, "Kube-score")
+
+ scriptPath := filepath.Join(getScriptFolder(), "kubescore-runner.sh")
+ args := []string{scriptPath, job.KuebConfigPath, job.Config.Namespace}
+ log.Error(scriptPath)
+
+ go func() {
+ // Use our custom script which is a wrapper around kubescore
+ cmd := exec.Command("bash", args...)
+ cmd.Dir = job.Folder
+ cmd.Env = make([]string, 0)
+ cmd.Env = append(cmd.Env, fmt.Sprintf("KUBECONFIG=%s", job.KuebConfigPath))
+
+ start := time.Now()
+ out, err := cmd.Output()
+ end := time.Now()
+
+ log.Info("Completed running kube-score")
+ log.Info(err)
+
+ // Remove any config files when done
+ job.RemoveTempFiles()
+
+ job.Duration = int(end.Sub(start).Seconds())
+
+ if err != nil {
+ // There was an error
+ // Remove the folder
+ os.Remove(job.Folder)
+ log.Error(">>>>>>>>> ERROR <<<<<<<<<")
+ log.Error(string(out))
+ log.Error(err)
+ job.Status = "error"
+ } else {
+ reportFile := filepath.Join(job.Folder, "repor.log")
+ ioutil.WriteFile(reportFile, out, os.ModePerm)
+ job.Status = "completed"
+ }
+ }()
+
+ return nil
+}
diff --git a/src/jetstream/plugins/analysis/container/main.go b/src/jetstream/plugins/analysis/container/main.go
new file mode 100644
index 0000000000..9dea04e2ea
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/main.go
@@ -0,0 +1,169 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/labstack/echo"
+ "github.com/labstack/echo/middleware"
+ log "github.com/sirupsen/logrus"
+)
+
+const (
+ defaultPort = 8090
+ defaultAddress = "0.0.0.0"
+ reportsDirEnvVar = "ANALYSIS_REPORTS_DIR"
+ scriptsDirEnvVar = "ANALYSIS_SCRIPTS_DIR"
+)
+
+type Analyzer struct {
+ reportsDir string
+ jobs map[string]*AnalysisJob
+}
+
+func main() {
+ log.SetFormatter(&log.TextFormatter{ForceColors: true, FullTimestamp: true, TimestampFormat: time.UnixDate})
+
+ log.SetOutput(os.Stdout)
+
+ log.Info("========================================")
+ log.Info("=== Stratos Analysis API Server ===")
+ log.Info("========================================")
+ log.Info("")
+ log.Info("Initialization started.")
+
+ analyzer := Analyzer{}
+ analyzer.jobs = make(map[string]*AnalysisJob)
+
+ analyzer.Start()
+}
+
+func (a *Analyzer) Start() {
+
+ // Reports folder
+
+ // Init reports directory
+ if reportsDir, ok := os.LookupEnv(reportsDirEnvVar); ok {
+ dir, err := filepath.Abs(reportsDir)
+ if err != nil {
+ log.Fatal("Can not get absolute path for reports folder")
+ }
+ a.reportsDir = dir
+ } else {
+ a.reportsDir = filepath.Join(os.TempDir(), "stratos-analysis")
+ }
+ log.Infof("Using reports folder: %s", a.reportsDir)
+
+ // Make the directory if it does not exit
+ if _, err := os.Stat(a.reportsDir); os.IsNotExist(err) {
+ if os.MkdirAll(a.reportsDir, os.ModePerm) != nil {
+ log.Fatal("Could not create folder for analysis reports")
+ }
+ }
+
+ // Start a simple web server
+ e := echo.New()
+ e.HideBanner = true
+ e.HidePort = true
+ customLoggerConfig := middleware.LoggerConfig{
+ Format: `Request: [${time_rfc3339}] Remote-IP:"${remote_ip}" ` +
+ `Method:"${method}" Path:"${path}" Status:${status} Latency:${latency_human} ` +
+ `Bytes-In:${bytes_in} Bytes-Out:${bytes_out}` + "\n",
+ }
+ e.Use(middleware.LoggerWithConfig(customLoggerConfig))
+ e.Use(middleware.Recover())
+
+ a.registerRoutes(e)
+
+ var engineErr error
+ address := fmt.Sprintf("%s:%d", defaultAddress, defaultPort)
+ log.Infof("Starting HTTP Server at address: %s", address)
+ engineErr = e.Start(address)
+
+ if engineErr != nil {
+ engineErrStr := fmt.Sprintf("%s", engineErr)
+ if !strings.Contains(engineErrStr, "Server closed") {
+ log.Warnf("Failed to start HTTP/S server: %+v", engineErr)
+ }
+ }
+}
+
+func (a *Analyzer) registerRoutes(e *echo.Echo) {
+ api := e.Group("/api")
+ api.Use(setSecureCacheContentMiddleware)
+
+ // Liveness check
+ api.GET("/v1/ping", a.ping)
+ // Run the given analyzer
+ api.POST("/v1/run/:analyzer", a.run)
+ // Get status
+ api.POST("/v1/status", a.status)
+ // Get a report
+ api.GET("/v1/report/:user/:endpoint/:id/:file", a.report)
+ // Delete a report
+ api.DELETE("/v1/report/:user/:endpoint/:id", a.delete)
+}
+
+func setSecureCacheContentMiddleware(h echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ c.Response().Header().Set("cache-control", "no-store")
+ c.Response().Header().Set("pragma", "no-cache")
+ return h(c)
+ }
+}
+
+// Set the name of the job
+func setJobNameAndPath(job *AnalysisJob, title string) {
+ job.Name = fmt.Sprintf("%s cluster analysis", title)
+ job.Path = ""
+
+ log.Info("setJobNameAndPath")
+ log.Infof("%+v", job.Config)
+
+ if job.Config != nil {
+ if len(job.Config.Namespace) > 0 {
+ if len(job.Config.App) > 0 {
+ job.Name = fmt.Sprintf("%s workload analysis: %s in %s", title, job.Config.App, job.Config.Namespace)
+ job.Path = fmt.Sprintf("%s/%s", job.Config.Namespace, job.Config.App)
+ } else {
+ job.Name = fmt.Sprintf("%s namespace analysis: %s", title, job.Config.Namespace)
+ job.Path = job.Config.Namespace
+ }
+ }
+ }
+}
+
+func getScriptFolder() string {
+ fallbackPath, err := os.Getwd()
+ if err != nil {
+ fallbackPath = "."
+ }
+
+ // Look first at the env var, then at a relative path to the executable
+ if dir, ok := os.LookupEnv(scriptsDirEnvVar); ok {
+ return dir
+ }
+
+ // Relative to the executable
+ dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
+ if err != nil {
+ log.Error("Could not get folder of the running program")
+ return fallbackPath
+ }
+
+ scripts := filepath.Join(dir, "scripts")
+ if _, err := os.Stat(scripts); !os.IsNotExist(err) {
+ return scripts
+ }
+
+ scripts = filepath.Join(dir, "pluginsĀ±", "analysis", "container", "scripts")
+ if _, err := os.Stat(scripts); !os.IsNotExist(err) {
+ return scripts
+ }
+
+ log.Error("Unable to locate scripts folder")
+ return fallbackPath
+}
diff --git a/src/jetstream/plugins/analysis/container/popeye.go b/src/jetstream/plugins/analysis/container/popeye.go
new file mode 100644
index 0000000000..d3c915b475
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/popeye.go
@@ -0,0 +1,115 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+)
+
+type popEyeSummary struct {
+ Score int `json:"score"`
+ Grade string `json:"grade"`
+}
+
+type popEyeResult struct {
+ PopEye popEyeSummary `json:"popeye"`
+}
+
+func runPopeye(job *AnalysisJob) error {
+
+ log.Debug("Running popeye job")
+
+ job.Busy = true
+ job.Type = "popeye"
+ job.Format = "popeye"
+ setJobNameAndPath(job, "Popeye")
+
+ log.Warn("Path is %s", job.Path)
+
+ args := []string{"--kubeconfig", job.KuebConfigPath, "-o", "json", "--insecure-skip-tls-verify"}
+ if len(job.Config.Namespace) > 0 {
+ args = append(args, "-n")
+ args = append(args, job.Config.Namespace)
+ } else {
+ args = append(args, "-A")
+ }
+
+ go func() {
+ log.Warn("===============================================================================")
+ log.Warnf("%+v", job)
+ log.Warn("===============================================================================")
+
+ cmd := exec.Command("popeye", args...)
+ cmd.Dir = job.Folder
+
+ start := time.Now()
+ out, err := cmd.Output()
+ end := time.Now()
+ job.EndTime = end
+
+ job.Busy = false
+ log.Info(start)
+ log.Info(end)
+
+ log.Info("Completed running popeye")
+ log.Info(err)
+
+ // Remove any config files when done
+ job.RemoveTempFiles()
+
+ job.Duration = int(end.Sub(start).Seconds())
+
+ if err != nil {
+ // There was an error
+ // Remove the folder
+ os.Remove(job.Folder)
+ job.Status = "error"
+ } else {
+ reportFile := filepath.Join(job.Folder, "report.json")
+ ioutil.WriteFile(reportFile, out, os.ModePerm)
+ job.Status = "completed"
+
+ // Parse the report
+ if summary, err := parsePopeyeReport(reportFile); err == nil {
+ job.Result = serializePopeyeReport(summary)
+ }
+ }
+ }()
+
+ return nil
+}
+
+func parsePopeyeReport(file string) (*popEyeSummary, error) {
+ jsonFile, err := os.Open(file)
+ if err != nil {
+ return nil, err
+ }
+ defer jsonFile.Close()
+
+ data, err := ioutil.ReadAll(jsonFile)
+ if err != nil {
+ return nil, err
+ }
+
+ result := popEyeResult{}
+ if err = json.Unmarshal(data, &result); err != nil {
+ return nil, errors.New("Failed to parse Popeye report")
+ }
+
+ return &result.PopEye, nil
+}
+
+func serializePopeyeReport(summary *popEyeSummary) string {
+ jsonString, err := json.Marshal(summary)
+ if err != nil {
+ return ""
+ }
+
+ return string(jsonString)
+}
diff --git a/src/jetstream/plugins/analysis/container/routes.go b/src/jetstream/plugins/analysis/container/routes.go
new file mode 100644
index 0000000000..ab90000a58
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/routes.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/labstack/echo"
+ log "github.com/sirupsen/logrus"
+)
+
+func (a *Analyzer) ping(ec echo.Context) error {
+
+ log.Info("PING!")
+
+ return nil
+
+}
+
+// Get a given report
+func (a *Analyzer) report(ec echo.Context) error {
+
+ user := ec.Param("user")
+ endpoint := ec.Param("endpoint")
+ id := ec.Param("id")
+ name := ec.Param("file")
+
+ // Name must end in json - we only serve json files
+ if !strings.HasSuffix(name, ".json") {
+ return errors.New("Can't serve that file")
+ }
+
+ file := filepath.Join(a.reportsDir, user, endpoint, id, name)
+ _, err := os.Stat(file)
+ if os.IsNotExist(err) {
+ return echo.NewHTTPError(404, "No such file")
+ }
+
+ return ec.File(file)
+}
+
+// Delete a given report
+func (a *Analyzer) delete(ec echo.Context) error {
+ log.Debug("delete report")
+
+ user := ec.Param("user")
+ endpoint := ec.Param("endpoint")
+ id := ec.Param("id")
+ folder := filepath.Join(a.reportsDir, user, endpoint, id)
+ if err := os.RemoveAll(folder); err != nil {
+ log.Warnf("Could not delete Analysis report folder: %s", folder)
+ }
+
+ return nil
+}
diff --git a/src/jetstream/plugins/analysis/container/run.go b/src/jetstream/plugins/analysis/container/run.go
new file mode 100644
index 0000000000..34089cb935
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/run.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+
+ "github.com/labstack/echo"
+ log "github.com/sirupsen/logrus"
+)
+
+const idHeaderName = "X-Stratos-Analaysis-ID"
+
+func (a *Analyzer) run(ec echo.Context) error {
+ err := a.doRun(ec)
+ if err != nil {
+ log.Error(err)
+ }
+ return err
+}
+
+func (a *Analyzer) doRun(ec echo.Context) error {
+
+ log.Debug("Run analyzer!")
+
+ engine := ec.Param("analyzer")
+ if len(engine) == 0 {
+ log.Warn("No analyzer")
+ return errors.New("No analyzer specified")
+ }
+
+ // ID is username/endpoint/id
+ id := ec.Request().Header.Get(idHeaderName)
+ if len(id) == 0 {
+ return errors.New("Mising ID header")
+ }
+
+ folder := filepath.Join(a.reportsDir, id)
+ if os.MkdirAll(folder, os.ModePerm) != nil {
+ return errors.New("Could not create folder for analysis report")
+ }
+
+ tempFiles := make([]string, 0)
+ reader, err := ec.Request().MultipartReader()
+ if err != nil {
+ log.Error("Could not parse request")
+ log.Error(err)
+ return errors.New("Failed to parse request payload")
+ }
+
+ job := AnalysisJob{}
+ params := kubeAnalyzerConfig{}
+
+ for {
+ part, err := reader.NextPart()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ log.Error("Unexpected error when retrieving a part of the message")
+ return errors.New("Unexpected error when retrieving a part of the message")
+ }
+ defer part.Close()
+ fileBytes, err := ioutil.ReadAll(part)
+ if err != nil {
+ log.Error("Failed to read content of the part")
+ return errors.New("Failed to read content of the part")
+ }
+ filename := part.Header.Get("Content-ID")
+
+ // Decide what to do with the part
+ switch filename {
+ case "job":
+ if err = json.Unmarshal(fileBytes, &job); err != nil {
+ return fmt.Errorf("Can not parse Job: %v", err)
+ }
+ case "body":
+ if err = json.Unmarshal(fileBytes, ¶ms); err != nil {
+ return fmt.Errorf("Can not parse parameters: %v", err)
+ }
+ log.Info("GOT JOB configuration")
+ job.Config = ¶ms
+ log.Infof("%+v", job.Config)
+ default:
+ fullpath := filepath.Join(folder, filename)
+ if err = ioutil.WriteFile(fullpath, fileBytes, os.ModePerm); err != nil {
+ return fmt.Errorf("Could not write file data for: %s", filename)
+ }
+ if filename == "kubeconfig" {
+ job.KuebConfigPath = fullpath
+ }
+ tempFiles = append(tempFiles, fullpath)
+ }
+ }
+
+ // TODO: Check we have job and params
+ if len(job.ID) == 0 {
+ return errors.New("Invalid Job metadata supplied")
+ }
+
+ job.Folder = folder
+ job.TempFiles = tempFiles
+
+ // Store the job so we track which jobs are running
+ a.jobs[job.ID] = &job
+
+ job.Status = "running"
+
+ switch engine {
+ case "popeye":
+ runPopeye(&job)
+ case "kube-score":
+ runKubeScore(&job)
+ case "clair":
+ runClair(&job)
+ // case "sonobuoy":
+ // runSonobuoy(dbStore, file, folder, report, requestBody)
+ default:
+ job.Status = "error"
+ return fmt.Errorf("Unkown analyzer: %s", engine)
+ }
+
+ return ec.JSON(http.StatusOK, job)
+}
diff --git a/src/jetstream/plugins/analysis/container/scripts/clair-runner.sh b/src/jetstream/plugins/analysis/container/scripts/clair-runner.sh
new file mode 100755
index 0000000000..c2b96f0fc6
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/scripts/clair-runner.sh
@@ -0,0 +1,60 @@
+ARGS="--all-namespaces"
+
+# When running in Kubernetes get the Clair server from the environment:
+# CLAIR_METRICS_API_SERVICE_PORT
+# CLAIR_METRICS_API_SERVICE_HOST
+
+if [ -n "${CLAIR_METRICS_API_SERVICE_HOST}" ]; then
+ echo "Setting CLAIR_ADDR from environment"
+ CLAIR_SERVER="http://${CLAIR_METRICS_API_SERVICE_HOST}:${CLAIR_METRICS_API_SERVICE_PORT}"
+else
+ if [ -z "${CLAIR_SERVER}" ]; then
+ echo "Need CLAIR_SERVER environment variable"
+ exit 1
+ fi
+fi
+
+echo "Clair server: ${CLAIR_SERVER}"
+
+# We use klar as this can be used in a Kubernetes environment without docker
+
+if [ -n "$2" ]; then
+ ARGS="-n ${2}"
+fi
+
+# $1 is the kubeconfig file
+
+echo "Enumerating all referenced images ..."
+
+# This gives us all of the images in the cluser or in the namespace
+IMAGES=$(kubectl get pods ${ARGS} -o jsonpath="{..image}" | tr -s '[[:space:]]' '\n' | sort | uniq)
+
+# Write the IMAGES list out
+echo "$IMAGES" > images.txt
+
+while IFS= read -r IMG; do
+ # Ignore empty image name
+ if [ -n ${IMG} ]; then
+ echo "Procesing image: ${IMG} ..."
+
+ # Create a filename that we can use for the log
+ # Replace / with underscore
+ LOGFILE="${IMG//\//_}"
+ LOGFILE="${LOGFILE/:/_}"
+
+ echo "Scanning ${IMG} ..."
+ export CLAIR_ADDR="${CLAIR_SERVER}"
+ export JSON_OUTPUT=false
+ export CLAIR_TIMEOUT=10
+ env
+ klar ${IMG} > ${LOGFILE}.log 2>&1
+ # Now JSON format (this will be quick as Clair will have cached the image data)
+ export JSON_OUTPUT=true
+ klar ${IMG} > ${LOGFILE}.json
+
+ # TODO: need to understand exit codes
+ fi
+
+done <<< "$IMAGES"
+
+exit 0
diff --git a/src/jetstream/plugins/analysis/container/scripts/kubescore-runner.sh b/src/jetstream/plugins/analysis/container/scripts/kubescore-runner.sh
new file mode 100755
index 0000000000..2763b3008f
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/scripts/kubescore-runner.sh
@@ -0,0 +1,16 @@
+ARGS="--all-namespaces"
+
+if [ -n "$2" ]; then
+ ARGS="-n ${2}"
+fi
+
+# $1 is the kubeconfig file
+
+echo "Kubescore runner..."
+echo "Running report..."
+
+kubectl api-resources --verbs=list --namespaced -o name \
+ | xargs -n1 -I{} bash -c "kubectl get {} $ARGS -oyaml && echo ---" \
+ | kube-score score -o json - > report.json
+
+exit 0
diff --git a/src/jetstream/plugins/analysis/container/scripts/sonobuoy-runner.sh b/src/jetstream/plugins/analysis/container/scripts/sonobuoy-runner.sh
new file mode 100755
index 0000000000..8565beed6f
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/scripts/sonobuoy-runner.sh
@@ -0,0 +1,19 @@
+# $1 is the kubeconfig file
+
+echo "Sonobuoy runner..."
+env
+echo "Args"
+echo $@
+
+echo "Running report..."
+
+# Run the report and wait
+sonobuoy run --wait
+
+# Retrieve the report
+
+# Teardown sonobuoy
+
+# Unpack the report and copy the junit report to report.json at the top-level
+
+exit 0
diff --git a/src/jetstream/plugins/analysis/container/status.go b/src/jetstream/plugins/analysis/container/status.go
new file mode 100644
index 0000000000..f2239e7b72
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/status.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "io/ioutil"
+
+ "github.com/labstack/echo"
+ log "github.com/sirupsen/logrus"
+)
+
+func (a *Analyzer) status(ec echo.Context) error {
+ err := a.doStatus(ec)
+ if err != nil {
+ log.Error(err)
+ }
+ return err
+}
+
+func (a *Analyzer) doStatus(ec echo.Context) error {
+ log.Debug("Status")
+ req := ec.Request()
+
+ // Body contains an array of IDs that the client thinks are running
+ // We send back updated status for each
+
+ // Get the list of IDs
+ defer req.Body.Close()
+ body, err := ioutil.ReadAll(req.Body)
+ if err != nil {
+ return errors.New("Could not read body")
+ }
+
+ ids := make([]string, 0)
+ if err := json.Unmarshal(body, &ids); err != nil {
+ return errors.New("Failed to parse body")
+ }
+
+ cleanup := make([]string, 0)
+
+ response := make(map[string]AnalysisJob)
+ for _, id := range ids {
+ if a.jobs[id] == nil {
+ // Client has a running job that we know nothing about - so must be an error
+ job := AnalysisJob{
+ ID: id,
+ Status: "error",
+ }
+ response[id] = job
+ } else {
+ response[id] = *a.jobs[id]
+
+ // If the job has finished, increement the cleanup counter
+ // We will remove it from our cache once we are pretty sure Jetstream has the status
+ if !a.jobs[id].Busy {
+ a.jobs[id].CleanupCounter = a.jobs[id].CleanupCounter + 1
+ if a.jobs[id].CleanupCounter > 5 {
+ cleanup = append(cleanup, id)
+ }
+ }
+ }
+
+ for _, id := range cleanup {
+ log.Errorf("Removing job >>>> %s", id)
+ delete(a.jobs, id)
+ }
+ }
+
+ ec.JSON(200, response)
+ return nil
+}
diff --git a/src/jetstream/plugins/analysis/container/types.go b/src/jetstream/plugins/analysis/container/types.go
new file mode 100644
index 0000000000..6e7b82e74b
--- /dev/null
+++ b/src/jetstream/plugins/analysis/container/types.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+ "encoding/json"
+ "os"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+)
+
+type kubeAnalyzerConfig struct {
+ Namespace string `json:"namespace"`
+ App string `json:"app"`
+}
+
+// AnalysisJob is the metadata format sent to and from the analyzer
+type AnalysisJob struct {
+ ID string `json:"id"`
+ UserID string `json:"-"`
+ EndpointType string `json:"endpointType"`
+ EndpointID string `json:"endpoint"`
+ Type string `json:"type"`
+ Path string `json:"path"`
+ Format string `json:"format"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ Duration int `json:"duration"`
+ Result string `json:"-"`
+ Summary *json.RawMessage `json:"summary"`
+ Config *kubeAnalyzerConfig `json:"-"`
+ Folder string `json:"-"`
+ KuebConfigPath string `json:"-"`
+ TempFiles []string `json:"-"`
+ Busy bool `json:"-"`
+ EndTime time.Time `json:"-"`
+ CleanupCounter int `json:"-"`
+}
+
+// RemoveTempFiles will remove any temporary files
+func (job *AnalysisJob) RemoveTempFiles() {
+ log.Debug("Removing temporary files")
+ for _, name := range job.TempFiles {
+ err := os.Remove(name)
+ if err != nil {
+ log.Error("Could not delete file: %s", name)
+ }
+ }
+}
diff --git a/src/jetstream/plugins/analysis/list.go b/src/jetstream/plugins/analysis/list.go
new file mode 100644
index 0000000000..cbfaa71be4
--- /dev/null
+++ b/src/jetstream/plugins/analysis/list.go
@@ -0,0 +1,238 @@
+package analysis
+
+import (
+ //"errors"
+
+ //"github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces"
+
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis/store"
+
+ "github.com/labstack/echo"
+
+ log "github.com/sirupsen/logrus"
+)
+
+const mainReportFile = "report.json"
+
+// listReports will list the analysis repotrs that have run
+func (c *Analysis) listReports(ec echo.Context) error {
+ log.Debug("listReports")
+ var p = c.portalProxy
+
+ // Need to get a config object for the target endpoint
+ // endpointGUID := ec.Param("endpoint")
+ userID := ec.Get("user_id").(string)
+
+ // Create a record in the reports datastore
+ dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection())
+ if err != nil {
+ return err
+ }
+
+ reports, err := dbStore.List(userID)
+ if err != nil {
+ return err
+ }
+
+ for _, report := range reports {
+ populateSummary(report)
+ }
+
+ return ec.JSON(200, reports)
+}
+
+// getReportsByPath will list the completed analysis repotrs that have run for the specified path
+func (c *Analysis) getReportsByPath(ec echo.Context) error {
+ log.Debug("getReportsByPath")
+ var p = c.portalProxy
+
+ // Need to get a config object for the target endpoint
+ // endpointGUID := ec.Param("endpoint")
+ userID := ec.Get("user_id").(string)
+ endpointID := ec.Param("endpoint")
+
+ log.Info("getReportsByPath")
+ log.Info(ec.Request().RequestURI)
+
+ pathPrefix := fmt.Sprintf("completed/%s/", endpointID)
+ index := strings.Index(ec.Request().RequestURI, pathPrefix)
+
+ log.Info(pathPrefix)
+ log.Info(index)
+ if index < 0 {
+ return errors.New("Invalid request")
+ }
+ path := ec.Request().RequestURI[index+len(pathPrefix):]
+
+ // Create a record in the reports datastore
+ dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection())
+ if err != nil {
+ return err
+ }
+
+ reports, err := dbStore.ListCompletedByPath(userID, endpointID, path)
+ if err != nil {
+ return err
+ }
+
+ for _, report := range reports {
+ populateSummary(report)
+ }
+
+ return ec.JSON(200, reports)
+}
+
+func populateSummary(report *store.AnalysisRecord) {
+ if len(report.Result) > 0 {
+ data := []byte(report.Result)
+ report.Summary = (*json.RawMessage)(&data)
+ }
+}
+
+func (c *Analysis) getLatestReport(ec echo.Context) error {
+ log.Debug("getLatestReport")
+ var p = c.portalProxy
+
+ // Need to get a config object for the target endpoint
+ userID := ec.Get("user_id").(string)
+ endpointID := ec.Param("endpoint")
+
+ pathPrefix := fmt.Sprintf("latest/%s/", endpointID)
+ index := strings.Index(ec.Request().RequestURI, pathPrefix)
+ if index < 0 {
+ return errors.New("Invalid request")
+ }
+ path := ec.Request().RequestURI[index+len(pathPrefix):]
+
+ // Create a record in the reports datastore
+ dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection())
+ if err != nil {
+ return err
+ }
+
+ report, err := dbStore.GetLatestCompleted(userID, endpointID, path)
+ if err != nil {
+ return echo.NewHTTPError(404, "No Analysis Report found")
+ }
+
+ if ec.Request().Method == "HEAD" {
+ ec.Response().Status = 200
+ return nil
+ }
+
+ // Get the report contents from the analysis server
+ bytes, err := c.getReportFile(report.UserID, report.EndpointID, report.ID, mainReportFile)
+ if err != nil {
+ return err
+ }
+
+ report.Report = (*json.RawMessage)(&bytes)
+ return ec.JSON(200, report)
+}
+
+func (c *Analysis) getReport(ec echo.Context) error {
+ log.Debug("getReport")
+ var p = c.portalProxy
+
+ // Need to get a config object for the target endpoint
+ userID := ec.Get("user_id").(string)
+ ID := ec.Param("id")
+ file := ec.Param("file")
+ if len(file) == 0 {
+ file = mainReportFile
+ }
+
+ // Create a record in the reports datastore
+ dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection())
+ if err != nil {
+ return err
+ }
+
+ report, err := dbStore.Get(userID, ID)
+ if err != nil {
+ return err
+ }
+
+ // Get the report contents from the analysis server
+ bytes, err := c.getReportFile(report.UserID, report.EndpointID, report.ID, file)
+ if err != nil {
+ return err
+ }
+
+ report.Report = (*json.RawMessage)(&bytes)
+ return ec.JSON(200, report)
+}
+
+func (c *Analysis) deleteReports(ec echo.Context) error {
+ log.Debug("deleteReports")
+ var p = c.portalProxy
+
+ // Need to get a config object for the target endpoint
+ userID := ec.Get("user_id").(string)
+
+ defer ec.Request().Body.Close()
+ body, err := ioutil.ReadAll(ec.Request().Body)
+ if err != nil {
+ return err
+ }
+
+ var ids []string
+ ids = make([]string, 0)
+ if err = json.Unmarshal(body, &ids); err != nil {
+ return err
+ }
+
+ dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection())
+ if err != nil {
+ return err
+ }
+
+ for _, id := range ids {
+ // Look up the report to get the endpoint ID
+ if job, err := dbStore.Get(userID, id); err == nil {
+ deleteURL := fmt.Sprintf("%s/api/v1/report/%s/%s/%s", c.analysisServer, job.UserID, job.EndpointID, job.ID)
+ r, _ := http.NewRequest(http.MethodDelete, deleteURL, nil)
+ client := &http.Client{Timeout: 30 * time.Second}
+ rsp, err := client.Do(r)
+ if err != nil {
+ log.Warnf("Could not delete analysis report for: %s", job.ID)
+ }
+ if rsp.StatusCode != http.StatusOK {
+ log.Warnf("Could not delete analysis report for: %s", job.ID)
+ }
+ }
+ dbStore.Delete(userID, id)
+ }
+
+ return ec.JSON(200, ids)
+}
+
+func (c *Analysis) getReportFile(userID, endpointID, ID, name string) ([]byte, error) {
+ // Make request to get report
+ statusURL := fmt.Sprintf("%s/api/v1/report/%s/%s/%s/%s", c.analysisServer, userID, endpointID, ID, name)
+ r, _ := http.NewRequest(http.MethodGet, statusURL, nil)
+ client := &http.Client{Timeout: 30 * time.Second}
+ rsp, err := client.Do(r)
+ if err != nil {
+ return nil, fmt.Errorf("Failed getting report from Analyzer service: %v", err)
+ }
+ if rsp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("Failed getting report from Analyzer service: %d", rsp.StatusCode)
+ }
+
+ defer rsp.Body.Close()
+ response, err := ioutil.ReadAll(rsp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("Could not read response: %v", err)
+ }
+
+ return response, nil
+}
diff --git a/src/jetstream/plugins/analysis/main.go b/src/jetstream/plugins/analysis/main.go
new file mode 100644
index 0000000000..b4bc28049d
--- /dev/null
+++ b/src/jetstream/plugins/analysis/main.go
@@ -0,0 +1,80 @@
+package analysis
+
+import (
+ "errors"
+
+ "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis/store"
+ "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces"
+
+ "github.com/labstack/echo"
+ log "github.com/sirupsen/logrus"
+)
+
+const analsyisServicesAPIEnvVar = "ANALYSIS_SERVICES_API"
+
+// Analysis - Plugin to allow analysers to run over an endpoint cluster
+type Analysis struct {
+ portalProxy interfaces.PortalProxy
+ analysisServer string
+}
+
+// Init creates a new Analysis
+func Init(portalProxy interfaces.PortalProxy) (interfaces.StratosPlugin, error) {
+ store.InitRepositoryProvider(portalProxy.GetConfig().DatabaseProviderName)
+ return &Analysis{portalProxy: portalProxy}, nil
+}
+
+// GetMiddlewarePlugin gets the middleware plugin for this plugin
+func (analysis *Analysis) GetMiddlewarePlugin() (interfaces.MiddlewarePlugin, error) {
+ return nil, errors.New("Not implemented")
+}
+
+// GetEndpointPlugin gets the endpoint plugin for this plugin
+func (analysis *Analysis) GetEndpointPlugin() (interfaces.EndpointPlugin, error) {
+ return nil, errors.New("Not implemented")
+}
+
+// GetRoutePlugin gets the route plugin for this plugin
+func (analysis *Analysis) GetRoutePlugin() (interfaces.RoutePlugin, error) {
+ return analysis, nil
+}
+
+// AddAdminGroupRoutes adds the admin routes for this plugin to the Echo server
+func (analysis *Analysis) AddAdminGroupRoutes(echoGroup *echo.Group) {
+ // no-op
+}
+
+// AddSessionGroupRoutes adds the session routes for this plugin to the Echo server
+func (analysis *Analysis) AddSessionGroupRoutes(echoGroup *echo.Group) {
+ echoGroup.GET("/analysis/reports", analysis.listReports)
+ echoGroup.GET("/analysis/reports/:id", analysis.getReport)
+ echoGroup.GET("/analysis/reports/:id/:file", analysis.getReport)
+
+ // Get completed reports for the given path
+ echoGroup.GET("/analysis/completed/:endpoint/*", analysis.getReportsByPath)
+
+ // Get latest report
+ echoGroup.GET("/analysis/latest/:endpoint/*", analysis.getLatestReport)
+ echoGroup.HEAD("/analysis/latest/:endpoint/*", analysis.getLatestReport)
+
+ echoGroup.DELETE("/analysis/reports", analysis.deleteReports)
+
+ // Run report
+ echoGroup.POST("/analysis/run/:analyzer/:endpoint", analysis.runReport)
+}
+
+// Init performs plugin initialization
+func (analysis *Analysis) Init() error {
+ log.Info("Analysis plugin loaded")
+
+ // Check env var
+ if url, ok := analysis.portalProxy.Env().Lookup(analsyisServicesAPIEnvVar); ok {
+ analysis.analysisServer = url
+
+ // Start background status check
+ analysis.initStatusCheck()
+ return nil
+ }
+
+ return errors.New("Analysis services API Server not configured")
+}
diff --git a/src/jetstream/plugins/analysis/run.go b/src/jetstream/plugins/analysis/run.go
new file mode 100644
index 0000000000..0895e5a0b4
--- /dev/null
+++ b/src/jetstream/plugins/analysis/run.go
@@ -0,0 +1,198 @@
+package analysis
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/textproto"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis/store"
+
+ "github.com/labstack/echo"
+ uuid "github.com/satori/go.uuid"
+ log "github.com/sirupsen/logrus"
+)
+
+type popeyeConfig struct {
+ Namespace string `json:"namespace"`
+ App string `json:"app"`
+}
+
+type KubeConfigExporter interface {
+ GetKubeConfigForEndpointUser(endpointID, userID string) (string, error)
+}
+
+const idHeaderName = "X-Stratos-Analaysis-ID"
+
+func (c *Analysis) runReport(ec echo.Context) error {
+ log.Debug("runReport")
+
+ analyzer := ec.Param("analyzer")
+ endpointID := ec.Param("endpoint")
+ userID := ec.Get("user_id").(string)
+
+ log.Warn(analyzer)
+ log.Warn(userID)
+ log.Warn(endpointID)
+
+ // For now we only support Popeye
+
+ // Look up the endpoint for the user
+ var p = c.portalProxy
+ endpoint, err := p.GetCNSIRecord(endpointID)
+ if err != nil {
+ return errors.New("Could not get endpoint information")
+ }
+
+ report := store.AnalysisRecord{
+ ID: uuid.NewV4().String(),
+ EndpointID: endpointID,
+ EndpointType: endpoint.CNSIType,
+ UserID: userID,
+ Path: "",
+ Created: time.Now(),
+ Read: false,
+ Duration: 0,
+ Status: "pending",
+ Result: "",
+ }
+
+ // Create a record in the reports datastore
+ dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection())
+ if err != nil {
+ return err
+ }
+
+ report.Name = fmt.Sprintf("Analysis report %s", analyzer)
+ dbStore.Save((report))
+
+ // Get Kube Config
+ k8s := c.portalProxy.GetPlugin("kubernetes")
+ if k8s == nil {
+ return errors.New("Could not find Kubernetes plugin")
+ }
+
+ k8sConfig, ok := k8s.(KubeConfigExporter)
+ if !ok {
+ return errors.New("Could not find Kubernetes plugin interface")
+ }
+
+ config, err := k8sConfig.GetKubeConfigForEndpointUser(endpointID, userID)
+ if err != nil {
+ return errors.New("Could not get Kube Config for the endpoint")
+ }
+
+ id := fmt.Sprintf("%s/%s/%s", userID, endpointID, report.ID)
+
+ // Create a multi-part form to send to the analyzer container
+
+ body := new(bytes.Buffer)
+ writer := multipart.NewWriter(body)
+
+ // Add kube config
+ metadataHeader := textproto.MIMEHeader{}
+ metadataHeader.Set("Content-Type", "application/yaml")
+ metadataHeader.Set("Content-ID", "kubeconfig")
+ part, _ := writer.CreatePart(metadataHeader)
+ part.Write([]byte(config))
+
+ requestBody := make([]byte, 0)
+
+ // Read body
+ defer ec.Request().Body.Close()
+ if b, err := ioutil.ReadAll((ec.Request().Body)); err == nil {
+ requestBody = b
+ }
+
+ // Content that was posted to us
+ postHeader := textproto.MIMEHeader{}
+ postHeader.Set("Content-Type", "application/json")
+ postHeader.Set("Content-ID", "body")
+ part, _ = writer.CreatePart(postHeader)
+ part.Write(requestBody)
+
+ // Report config
+ reportHeader := textproto.MIMEHeader{}
+ reportHeader.Set("Content-Type", "application/json")
+ reportHeader.Set("Content-ID", "job")
+ part, _ = writer.CreatePart(reportHeader)
+ job, err := json.Marshal(report)
+ if err != nil {
+ return errors.New("Could not serialize job")
+ }
+ part.Write(job)
+ writer.Close()
+
+ // Post this to the Analyzer API
+ contentType := fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary())
+ uploadURL := fmt.Sprintf("%s/api/v1/run/%s", c.analysisServer, analyzer)
+ r, _ := http.NewRequest(http.MethodPost, uploadURL, bytes.NewReader(body.Bytes()))
+ r.Header.Set("Content-Type", contentType)
+ r.Header.Set(idHeaderName, id)
+ client := &http.Client{Timeout: 180 * time.Second}
+ rsp, err := client.Do(r)
+ if err != nil {
+ report.Status = "error"
+ dbStore.UpdateReport(userID, &report)
+ return errors.New("Analysis job failed - could not contact Analysis Server")
+ }
+ if rsp.StatusCode != http.StatusOK {
+ log.Debugf("Request failed with response code: %d", rsp.StatusCode)
+ report.Status = "error"
+ dbStore.UpdateReport(userID, &report)
+ return errors.New("Analysis job failed")
+ }
+
+ // Job submitted okay
+ // Updated job is in the response
+
+ defer rsp.Body.Close()
+ response, err := ioutil.ReadAll(rsp.Body)
+ if err != nil {
+ report.Status = "error"
+ dbStore.UpdateReport(userID, &report)
+ return errors.New("Could not read response")
+ }
+ updatedJob := store.AnalysisRecord{}
+ if err = json.Unmarshal(response, &updatedJob); err != nil {
+ report.Status = "error"
+ dbStore.UpdateReport(userID, &report)
+ return errors.New("Could not read response - could not deserialize response")
+ }
+
+ report.Duration = updatedJob.Duration
+ report.Status = updatedJob.Status
+ report.Name = updatedJob.Name
+ report.Format = updatedJob.Format
+ report.Type = updatedJob.Type
+ report.Path = updatedJob.Path
+
+ log.Debug("OK => Job submitted okay")
+ log.Debug("=======================================================")
+ log.Debugf("%+v", report)
+ log.Debug("=======================================================")
+
+ err = dbStore.UpdateReport(userID, &report)
+ if err != nil {
+ return errors.New("Could not save report")
+ }
+
+ log.Debug("All done - job saved")
+
+ return ec.JSON(200, report)
+}
+
+func getScriptFolder() string {
+ dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
+ if err != nil {
+ return "."
+ }
+ return filepath.Join(dir, "plugins", "analysis", "scripts")
+}
diff --git a/src/jetstream/plugins/analysis/status.go b/src/jetstream/plugins/analysis/status.go
new file mode 100644
index 0000000000..9b990aa57d
--- /dev/null
+++ b/src/jetstream/plugins/analysis/status.go
@@ -0,0 +1,102 @@
+package analysis
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "time"
+
+ "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis/store"
+
+ log "github.com/sirupsen/logrus"
+)
+
+// Start a poller to check the status
+func (c *Analysis) initStatusCheck() {
+
+ log.Info("Starting status check ...")
+
+ // Just loop forever, checking the status of running jobs every 10s
+ go func() {
+ for {
+ time.Sleep(10 * time.Second)
+ err := c.checkStatus()
+ if err != nil {
+ log.Errorf("Error checking status: %v", err)
+ }
+ }
+ }()
+}
+
+func (c *Analysis) checkStatus() error {
+ log.Debug("Checking status....")
+ p := c.portalProxy
+ // Create a record in the reports datastore
+ dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection())
+ if err != nil {
+ return fmt.Errorf("Status Check: Can not get anaylsis store db: %v", err)
+ }
+
+ // Get all running jobs
+ running, err := dbStore.ListRunning()
+ if err != nil {
+ return fmt.Errorf("Can not get list of running jobs: %v", err)
+ }
+
+ if len(running) == 0 {
+ return nil
+ }
+
+ ids := make([]string, 0)
+ for _, job := range running {
+ log.Infof("Got running job: %s", job.ID)
+ ids = append(ids, job.ID)
+ }
+
+ data, err := json.Marshal(ids)
+ if err != nil {
+ log.Errorf("Could not marshal IDs: %v", err)
+ return fmt.Errorf("Could not marshal IDs: %v", err)
+ }
+
+ // Make request to status
+ statusURL := fmt.Sprintf("%s/api/v1/status", c.analysisServer)
+ r, _ := http.NewRequest(http.MethodPost, statusURL, bytes.NewReader(data))
+ r.Header.Set("Content-Type", "application/json")
+ client := &http.Client{Timeout: 180 * time.Second}
+ rsp, err := client.Do(r)
+ if err != nil {
+ return fmt.Errorf("Failed getting status from Analyzer service: %v", err)
+ }
+ if rsp.StatusCode != http.StatusOK {
+ return fmt.Errorf("Failed getting status from Analyzer service: %d", rsp.StatusCode)
+ }
+
+ defer rsp.Body.Close()
+ response, err := ioutil.ReadAll(rsp.Body)
+ if err != nil {
+ log.Errorf("Could not read response: %v", err)
+ return fmt.Errorf("Could not read response: %v", err)
+ }
+
+ // Turn into map of IDs to Jobs
+ statuses := make(map[string]store.AnalysisRecord)
+
+ if err := json.Unmarshal(response, &statuses); err != nil {
+ return fmt.Errorf("Could not parse response: %v", err)
+ }
+
+ for _, job := range running {
+ if status, ok := statuses[job.ID]; ok {
+ job.Duration = status.Duration
+ job.Status = status.Status
+ if err := dbStore.UpdateReport(job.UserID, job); err != nil {
+ log.Warnf("Unable to update status for job %s: %v", job.ID, err)
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/src/jetstream/plugins/analysis/store/analysis_store_db.go b/src/jetstream/plugins/analysis/store/analysis_store_db.go
new file mode 100644
index 0000000000..dcd3c979e1
--- /dev/null
+++ b/src/jetstream/plugins/analysis/store/analysis_store_db.go
@@ -0,0 +1,164 @@
+package store
+
+import (
+ "database/sql"
+ "fmt"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/cloudfoundry-incubator/stratos/src/jetstream/datastore"
+)
+
+var (
+ listReports = `SELECT id, endpoint_type, endpoint, user, name, path, type, format, created, acknowledged, status, duration, result FROM analysis WHERE user = $1`
+ listCompletedReportsByPath = `SELECT id, endpoint_type, endpoint, user, name, path, type, format, created, acknowledged, status, duration, result FROM analysis WHERE status = 'completed' AND user = $1 AND endpoint = $2 AND path = $3 ORDER BY created DESC`
+ getReport = `SELECT id, endpoint_type, endpoint, user, name, path, type, format, created, acknowledged, status, duration, result FROM analysis WHERE user = $1 AND id=$2`
+ deleteReport = `DELETE FROM analysis WHERE user = $1 AND id = $2`
+ saveReport = `INSERT INTO analysis (id, user, endpoint_type, endpoint, name, path, type, format, created, acknowledged, status, duration, result) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`
+ updateReport = `UPDATE analysis SET type = $1, format = $2, acknowledged = $3, status = $4, duration = $5, result = $6, name = $7, path = $8, result = $9 WHERE user = $10 AND id = $11`
+ getLatestReport = `SELECT id, endpoint_type, endpoint, user, name, path, type, format, created, acknowledged, status, duration, result FROM analysis WHERE status = 'completed' AND user = $1 AND endpoint = $2 AND path = $3 ORDER BY created DESC`
+ listRunningReports = `SELECT id, endpoint_type, endpoint, user, name, path, type, format, created, acknowledged, status, duration, result FROM analysis WHERE status = 'running' ORDER BY created DESC`
+
+ // TODO: Delete for endpoint when endpoint is removed?
+)
+
+// InitRepositoryProvider - One time init for the given DB Provider
+func InitRepositoryProvider(databaseProvider string) {
+ // Modify the database statements if needed, for the given database type
+ listReports = datastore.ModifySQLStatement(listReports, databaseProvider)
+ listCompletedReportsByPath = datastore.ModifySQLStatement(listCompletedReportsByPath, databaseProvider)
+ getReport = datastore.ModifySQLStatement(getReport, databaseProvider)
+ deleteReport = datastore.ModifySQLStatement(deleteReport, databaseProvider)
+ saveReport = datastore.ModifySQLStatement(saveReport, databaseProvider)
+ updateReport = datastore.ModifySQLStatement(updateReport, databaseProvider)
+ getLatestReport = datastore.ModifySQLStatement(getLatestReport, databaseProvider)
+ listRunningReports = datastore.ModifySQLStatement(listRunningReports, databaseProvider)
+}
+
+// AnalysisDBStore is a DB-backed Analysis Reports repository
+type AnalysisDBStore struct {
+ db *sql.DB
+}
+
+// NewAnalysisDBStore will create a new instance of the AnalysisDBStore
+func NewAnalysisDBStore(dcp *sql.DB) (AnalysisStore, error) {
+ return &AnalysisDBStore{db: dcp}, nil
+}
+
+// List - Returns a list of all user Analysis Reports
+func (p *AnalysisDBStore) List(userGUID string) ([]*AnalysisRecord, error) {
+ log.Debug("List")
+ rows, err := p.db.Query(listReports, userGUID)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to retrieve Analysis Reports records: %v", err)
+ }
+ defer rows.Close()
+
+ return list(rows)
+}
+
+func (p *AnalysisDBStore) ListCompletedByPath(userGUID, endpointID, path string) ([]*AnalysisRecord, error) {
+ log.Debug("ListCompletedByPath")
+ rows, err := p.db.Query(listCompletedReportsByPath, userGUID, endpointID, path)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to retrieve Analysis Reports records: %v", err)
+ }
+ defer rows.Close()
+
+ return list(rows)
+}
+
+func (p *AnalysisDBStore) ListRunning() ([]*AnalysisRecord, error) {
+ log.Debug("ListRunning")
+ rows, err := p.db.Query(listRunningReports)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to retrieve Analysis Reports records: %v", err)
+ }
+ defer rows.Close()
+
+ return list(rows)
+}
+
+func list(rows *sql.Rows) ([]*AnalysisRecord, error) {
+ var reportList []*AnalysisRecord
+ reportList = make([]*AnalysisRecord, 0)
+
+ for rows.Next() {
+ report := new(AnalysisRecord)
+ err := rows.Scan(&report.ID, &report.EndpointType, &report.EndpointID, &report.UserID, &report.Name, &report.Path, &report.Type, &report.Format, &report.Created, &report.Read, &report.Status, &report.Duration, &report.Result)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to scan Analysis Reports records: %v", err)
+ }
+ reportList = append(reportList, report)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("Unable to List Analysis Reports records: %v", err)
+ }
+
+ return reportList, nil
+}
+
+// Get - Get a specific Analysis Report by ID
+func (p *AnalysisDBStore) Get(userGUID, ID string) (*AnalysisRecord, error) {
+ log.Debug("Get")
+
+ report := AnalysisRecord{}
+ err := p.db.QueryRow(getReport, userGUID, ID).Scan(&report.ID, &report.EndpointType, &report.EndpointID, &report.UserID, &report.Name, &report.Path, &report.Type, &report.Format, &report.Created, &report.Read, &report.Status, &report.Duration, &report.Result)
+ if err != nil {
+ msg := "Unable to Get Analysis Report record: %v"
+ log.Debugf(msg, err)
+ return nil, fmt.Errorf(msg, err)
+ }
+
+ return &report, nil
+}
+
+// GetLatestCompleted - Get latest report for the specified path
+func (p *AnalysisDBStore) GetLatestCompleted(userGUID, endpointID, path string) (*AnalysisRecord, error) {
+ log.Debug("GetLatestCompleted")
+
+ report := AnalysisRecord{}
+ err := p.db.QueryRow(getLatestReport, userGUID, endpointID, path).Scan(&report.ID, &report.EndpointType, &report.EndpointID, &report.UserID, &report.Name, &report.Path, &report.Type, &report.Format, &report.Created, &report.Read, &report.Status, &report.Duration, &report.Result)
+ if err != nil {
+ msg := "Unable to get laetst completed Analysis Report record: %v"
+ log.Debugf(msg, err)
+ return nil, fmt.Errorf(msg, err)
+ }
+
+ return &report, nil
+}
+
+// Delete will delete an Analysis Report from the datastore
+func (p *AnalysisDBStore) Delete(userGUID string, id string) error {
+ if _, err := p.db.Exec(deleteReport, userGUID, id); err != nil {
+ return fmt.Errorf("Unable to delete Analysis Report record: %v", err)
+ }
+
+ return nil
+}
+
+// UpdateReport will update the dynamic fields of the Analysis Record in thedatastore
+func (p *AnalysisDBStore) UpdateReport(userGUID string, report *AnalysisRecord) error {
+ if _, err := p.db.Exec(updateReport, report.Type, report.Format, report.Read, report.Status, report.Duration, report.Result, report.Name, report.Path, report.Result, userGUID, report.ID); err != nil {
+ return fmt.Errorf("Unable to update Analysis Report record: %v", err)
+ }
+ return nil
+}
+
+// Save will persist an Analysis Report to the datastore
+func (p *AnalysisDBStore) Save(report AnalysisRecord) (*AnalysisRecord, error) {
+ if _, err := p.db.Exec(saveReport, report.ID, report.UserID, report.EndpointType, report.EndpointID, report.Name, report.Path, report.Type, report.Format, report.Created, report.Read, &report.Status, &report.Duration, &report.Result); err != nil {
+ return nil, fmt.Errorf("Unable to save Analysis Report record: %v", err)
+ }
+
+ return &report, nil
+}
+
+// DeleteFromEndpoint will remove all Analysis Reports for a given endpoint guid
+// func (p *AnalysisDBStore) DeleteFromEndpoint(endpointGUID string) error {
+// if _, err := p.db.Exec(deleteEndpointFavorite, endpointGUID); err != nil {
+// return fmt.Errorf("Unable to User Favorite record: %v", err)
+// }
+// return nil
+// }
diff --git a/src/jetstream/plugins/analysis/store/main.go b/src/jetstream/plugins/analysis/store/main.go
new file mode 100644
index 0000000000..6b81e86c6d
--- /dev/null
+++ b/src/jetstream/plugins/analysis/store/main.go
@@ -0,0 +1,37 @@
+package store
+
+import (
+ "encoding/json"
+ "time"
+)
+
+// AnalysisRecord represents an analysis that has been run
+type AnalysisRecord struct {
+ ID string `json:"id"`
+ UserID string `json:"-"`
+ EndpointType string `json:"endpointType"`
+ EndpointID string `json:"endpoint"`
+ Type string `json:"type"`
+ Format string `json:"format"`
+ Name string `json:"name"`
+ Path string `json:"path"`
+ Created time.Time `json:"created"`
+ Read bool `json:"read"`
+ Status string `json:"status"`
+ Duration int `json:"duration"`
+ Result string `json:"-"`
+ Summary *json.RawMessage `json:"summary"`
+ Report *json.RawMessage `json:"report,omitempty"`
+}
+
+// AnalysisStore is the analysis repository
+type AnalysisStore interface {
+ List(userGUID string) ([]*AnalysisRecord, error)
+ Get(userGUID, id string) (*AnalysisRecord, error)
+ GetLatestCompleted(userGUID, endpointID, path string) (*AnalysisRecord, error)
+ ListCompletedByPath(userGUID, endpointID, path string) ([]*AnalysisRecord, error)
+ ListRunning() ([]*AnalysisRecord, error)
+ Delete(userGUID, id string) error
+ Save(record AnalysisRecord) (*AnalysisRecord, error)
+ UpdateReport(userGUID string, report *AnalysisRecord) error
+}
diff --git a/src/jetstream/plugins/kubernetes/endpoint_config.go b/src/jetstream/plugins/kubernetes/endpoint_config.go
index 8a7acd85af..aada4d4938 100644
--- a/src/jetstream/plugins/kubernetes/endpoint_config.go
+++ b/src/jetstream/plugins/kubernetes/endpoint_config.go
@@ -40,6 +40,25 @@ func (c *KubernetesSpecification) GetConfigForEndpointUser(endpointID, userID st
return c.GetConfigForEndpoint(cnsiRecord.APIEndpoint.String(), tokenRec)
}
+func (c *KubernetesSpecification) GetKubeConfigForEndpointUser(endpointID, userID string) (string, error) {
+
+ var p = c.portalProxy
+
+ cnsiRecord, err := p.GetCNSIRecord(endpointID)
+ if err != nil {
+ //return sendSSHError("Could not get endpoint information")
+ return "", errors.New("Could not get endpoint information")
+ }
+
+ // Get token for this users
+ tokenRec, ok := p.GetCNSITokenRecord(endpointID, userID)
+ if !ok {
+ return "", errors.New("Could not get token")
+ }
+
+ return c.GetKubeConfigForEndpoint(cnsiRecord.APIEndpoint.String(), tokenRec, "")
+}
+
func (c *KubernetesSpecification) getKubeConfigForEndpoint(masterURL string, token interfaces.TokenRecord, namespace string) (*clientcmdapi.Config, error) {
name := "cluster-0"
diff --git a/src/jetstream/plugins/kubernetes/helm/release.go b/src/jetstream/plugins/kubernetes/helm/release.go
index 9c15554514..16f229c77d 100644
--- a/src/jetstream/plugins/kubernetes/helm/release.go
+++ b/src/jetstream/plugins/kubernetes/helm/release.go
@@ -232,6 +232,8 @@ func (r *HelmRelease) UpdatePods(jetstream interfaces.PortalProxy) {
podCopy := &v1.Pod{}
*podCopy = pod
+ podCopy.Kind = "Pod"
+ podCopy.APIVersion = "v1"
res.Resource = podCopy
pods[res.getID()] = &res