diff --git a/.circleci/config.yml b/.circleci/config.yml index 781eae369..b4a62107a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,13 +60,13 @@ jobs: chmod +x komiser_windows_amd64.exe komiser_darwin_amd64 komiser_linux_amd64 - run: name: Push Linux binary - command: aws s3 cp komiser_linux_amd64 s3://komiser/2.1.0/linux/komiser --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers + command: aws s3 cp komiser_linux_amd64 s3://komiser/2.2.0/linux/komiser --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers - run: name: Push Windows binary - command: aws s3 cp komiser_windows_amd64.exe s3://komiser/2.1.0/windows/komiser --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers + command: aws s3 cp komiser_windows_amd64.exe s3://komiser/2.2.0/windows/komiser --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers - run: name: Push Mac OS X binary - command: aws s3 cp komiser_darwin_amd64 s3://komiser/2.1.0/osx/komiser --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers + command: aws s3 cp komiser_darwin_amd64 s3://komiser/2.2.0/osx/komiser --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers - run: name: Upload IAM policy command: aws s3 cp policy.json s3://komiser/policy.json --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers diff --git a/Dockerfile b/Dockerfile index 836b81aa5..a7cc2bb0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM alpine:3.9.4 MAINTAINER mlabouardy -ENV VERSION 2.1.0 +ENV VERSION 2.2.0 ENV PORT 3000 ENV DURATION 30 @@ -11,4 +11,4 @@ RUN curl -L https://s3.us-east-1.amazonaws.com/komiser/$VERSION/linux/komiser -o mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 EXPOSE $PORT -ENTRYPOINT komiser start --port $PORT +ENTRYPOINT ["komiser", "start"] diff --git a/README.md b/README.md index a582fc91e..0f82893fa 100644 --- a/README.md +++ b/README.md @@ -28,24 +28,24 @@ Stay under budget by uncovering hidden costs, monitoring increases in spend, and ## Download -Below are the available downloads for the latest version of Komiser (2.1.0). Please download the proper package for your operating system and architecture. +Below are the available downloads for the latest version of Komiser (2.2.0). Please download the proper package for your operating system and architecture. ### Linux: ``` -wget https://cli.komiser.io/2.1.0/linux/komiser +wget https://cli.komiser.io/2.2.0/linux/komiser ``` ### Windows: ``` -wget https://cli.komiser.io/2.1.0/windows/komiser +wget https://cli.komiser.io/2.2.0/windows/komiser ``` ### Mac OS X: ``` -wget https://cli.komiser.io/2.1.0/osx/komiser +wget https://cli.komiser.io/2.2.0/osx/komiser ``` Docker for Mac is best installed with Homebrew: @@ -60,7 +60,7 @@ _Note_: make sure to add the execution permission to Komiser `chmod +x komiser` ### Docker: ``` -docker run -d -p 3000:3000 -e AWS_ACCESS_KEY_ID="" -e AWS_SECRET_ACCESS_KEY="" -e AWS_DEFAULT_REGION="" --name komiser mlabouardy/komiser:2.1.0 +docker run -d -p 3000:3000 -e AWS_ACCESS_KEY_ID="" -e AWS_SECRET_ACCESS_KEY="" -e AWS_DEFAULT_REGION="" --name komiser mlabouardy/komiser:2.2.0 ``` ## How to use @@ -100,6 +100,38 @@ komiser start --port 3000 --redis localhost:6379 --duration 30

+#### Multiple AWS Accounts Support + +Komiser support multiple AWS accounts through named profiles that are stored in the `config` and `credentials files`. You can configure additional profiles by using `aws configure` with the `--profile` option, or by adding entries to the `config` and `credentials` files. + +The following example shows a credentials file with 3 profiles (production, staging & sandbox accounts): + +``` +[Production] +aws_access_key_id= +aws_secret_access_key= + +[Staging] +aws_access_key_id= +aws_secret_access_key= + +[Sandbox] +aws_access_key_id= +aws_secret_access_key= +``` + +To enable multiple AWS accounts feature, add the --multiple option to Komiser: + +``` +komiser start --port 3000 --redis localhost:6379 --duration 30 --multiple +``` + +* If you point your browser to http://localhost:3000, you should be able to see your accounts: + +

+ +

+ ### GCP * Create a service account with *Viewer* permission, see [Creating and managing service accounts](https://cloud.google.com/iam/docs/creating-managing-service-accounts) docs. @@ -153,6 +185,7 @@ komiser start [OPTIONS] --duration value, -d value Cache expiration time (default: 30 minutes) --redis value, -r value Redis server (localhost:6379) --dataset value, -ds value BigQuery dataset name (project-id.dataset-name.table-name) + --multiple, -m Enable multiple AWS accounts feature ``` ## Configuring Credentials diff --git a/dashboard/src/app/app.component.html b/dashboard/src/app/app.component.html index 7671c8b94..ae627b7ee 100644 --- a/dashboard/src/app/app.component.html +++ b/dashboard/src/app/app.component.html @@ -58,7 +58,7 @@
  • - + See all notifications @@ -138,11 +138,19 @@ bottom: 0vh; display: block; width: 100%;"> - + +
    + + +
    + diff --git a/dashboard/src/app/app.component.ts b/dashboard/src/app/app.component.ts index a463f696d..0379fe349 100644 --- a/dashboard/src/app/app.component.ts +++ b/dashboard/src/app/app.component.ts @@ -4,6 +4,7 @@ import { GcpService } from './gcp.service'; import { StoreService } from './store.service'; import { not } from '@angular/compiler/src/output/output_ast'; import { Subscription } from 'rxjs'; +import { Subject } from "rxjs/Subject"; import * as moment from 'moment'; declare var ga: Function; @@ -17,10 +18,12 @@ export class AppComponent implements OnDestroy { public accountName: string = 'Username'; public redAlarms: number; + public profiles: Array = []; + public currentProfile: string; public notifications: Array = []; public _subscription: Subscription; public currentProvider: any; - public availableProviders : Array = [ + public availableProviders: Array = [ { label: 'Amazon Web Services', value: 'aws' @@ -29,13 +32,13 @@ export class AppComponent implements OnDestroy { label: 'Google Cloud Platform', value: 'gcp' } - ] + ]; private _storeService: StoreService; private providers: Map = new Map(); - constructor(private awsService: AwsService, private gcpService: GcpService, private storeService: StoreService){ + constructor(private awsService: AwsService, private gcpService: GcpService, private storeService: StoreService) { this.providers['aws'] = { label: 'Amazon Web Services', @@ -49,6 +52,26 @@ export class AppComponent implements OnDestroy { logo: 'https://cdn.komiser.io/images/gcp.png' }; + //if (this.storeService.getProvider() == 'aws') { + if (localStorage.getItem('profile')) { + this.currentProfile = localStorage.getItem('profile'); + } else { + this.currentProfile = 'default'; + localStorage.setItem('profile', this.currentProfile); + } + + this.awsService.getProfiles().subscribe(profiles => { + this.profiles = profiles; + if (this.profiles.length > 0 && this.profiles.indexOf(this.currentProfile) == -1) { + this.currentProfile = this.profiles[0]; + localStorage.setItem('profile', this.currentProfile); + } + }, err => { + this.profiles = []; + }) + // } + + this.currentProvider = this.providers[this.storeService.getProvider()]; this.storeService.onProviderChanged(this.storeService.getProvider()); @@ -64,22 +87,21 @@ export class AppComponent implements OnDestroy { }) } - private getAccountName(){ - console.log(this.currentProvider); - if (this.currentProvider.value == 'aws'){ + private getAccountName() { + if (this.currentProvider.value == 'aws') { this.awsService.getAccountName().subscribe(data => { this.accountName = data.username; }, err => { this.accountName = 'Username'; }); - + this.awsService.getCloudwatchAlarms().subscribe(data => { this.redAlarms = data.ALARM; }, err => { this.redAlarms = 0; }); } else { - this.redAlarms = 0; + this.redAlarms = 0; this.gcpService.getProjects().subscribe(data => { this.accountName = data[0].name; @@ -90,17 +112,24 @@ export class AppComponent implements OnDestroy { } ngOnDestroy() { - this._subscription.unsubscribe(); - } - - public calcMoment(timestamp){ - return moment(timestamp).fromNow(); - } - - public onCloudProviderSelected(provider){ - this.currentProvider = this.providers[provider]; - this._storeService.onProviderChanged(provider); - this.getAccountName(); - } + this._subscription.unsubscribe(); + } + + public calcMoment(timestamp) { + return moment(timestamp).fromNow(); + } + + public onCloudProviderSelected(provider) { + this.currentProvider = this.providers[provider]; + this._storeService.onProviderChanged(provider); + this.getAccountName(); + } + + public onProfileSelected(profile) { + this.currentProfile = profile; + localStorage.setItem('profile', this.currentProfile); + this._storeService.onProfileChanged(profile); + this.getAccountName(); + } } diff --git a/dashboard/src/app/app.module.ts b/dashboard/src/app/app.module.ts index 1980175e6..6cafa2965 100644 --- a/dashboard/src/app/app.module.ts +++ b/dashboard/src/app/app.module.ts @@ -37,6 +37,7 @@ import { AwsLimitsComponent } from './limits/aws/aws.component'; import { GcpLimitsComponent } from './limits/gcp/gcp.component'; import { AwsProfileComponent } from './profile/aws/aws.component'; import { GcpProfileComponent } from './profile/gcp/gcp.component'; +import { NotificationsComponent } from './notifications/notifications.component'; @@ -76,6 +77,11 @@ const appRoutes: Routes = [ component: LimitsComponent, data: { title: 'Service Limits Checks - Komiser' } }, + { + path: 'notifications', + component: NotificationsComponent, + data: { title: 'Notifications - Komiser' } + }, { path: '', component: DashboardComponent, data: { title: 'Dashboard - Komiser' } @@ -108,7 +114,8 @@ const appRoutes: Routes = [ AwsLimitsComponent, GcpLimitsComponent, AwsProfileComponent, - GcpProfileComponent + GcpProfileComponent, + NotificationsComponent ], imports: [ RouterModule.forRoot( diff --git a/dashboard/src/app/aws.service.ts b/dashboard/src/app/aws.service.ts index 096b8be5a..e67ad5254 100644 --- a/dashboard/src/app/aws.service.ts +++ b/dashboard/src/app/aws.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Http } from '@angular/http'; +import { Http, Headers } from '@angular/http'; import 'rxjs/add/operator/map'; import { Observable } from "rxjs/Rx"; import { StoreService } from './store.service'; @@ -11,9 +11,23 @@ export class AwsService { constructor(private http: Http, private storeService: StoreService) { } + public getProfiles(){ + return this.http + .get(`${this.BASE_URL}/profiles`) + .map(res => { + return res.json() + }) + .catch(err => { + let payload = JSON.parse(err._body) + if (payload && payload.error) + this.storeService.add(payload.error); + return Observable.throw(err.json().error) + }) + } + public getCurrentCost(){ return this.http - .get(`${this.BASE_URL}/cost/current`) + .get(`${this.BASE_URL}/cost/current`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -27,7 +41,7 @@ export class AwsService { public getCostAndUsage(){ return this.http - .get(`${this.BASE_URL}/cost/history`) + .get(`${this.BASE_URL}/cost/history`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -41,7 +55,7 @@ export class AwsService { public getIAMUsers(){ return this.http - .get(`${this.BASE_URL}/iam/users`) + .get(`${this.BASE_URL}/iam/users`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -55,7 +69,7 @@ export class AwsService { public getInstancesPerRegion(){ return this.http - .get(`${this.BASE_URL}/ec2/regions`) + .get(`${this.BASE_URL}/ec2/regions`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -69,7 +83,7 @@ export class AwsService { public getUsedRegions(){ return this.http - .get(`${this.BASE_URL}/resources/regions`) + .get(`${this.BASE_URL}/resources/regions`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -83,7 +97,7 @@ export class AwsService { public getCloudwatchAlarms(){ return this.http - .get(`${this.BASE_URL}/cloudwatch/alarms`) + .get(`${this.BASE_URL}/cloudwatch/alarms`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -97,7 +111,7 @@ export class AwsService { public getLambdaFunctions(){ return this.http - .get(`${this.BASE_URL}/lambda/functions`) + .get(`${this.BASE_URL}/lambda/functions`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -111,7 +125,7 @@ export class AwsService { public getLambdaInvocationMetrics(){ return this.http - .get(`${this.BASE_URL}/lambda/invocations`) + .get(`${this.BASE_URL}/lambda/invocations`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -125,7 +139,7 @@ export class AwsService { public getAccountName(){ return this.http - .get(`${this.BASE_URL}/iam/account`) + .get(`${this.BASE_URL}/iam/account`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -139,7 +153,7 @@ export class AwsService { public getNumberOfS3Buckets(){ return this.http - .get(`${this.BASE_URL}/s3/buckets`) + .get(`${this.BASE_URL}/s3/buckets`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -153,7 +167,7 @@ export class AwsService { public getBucketObjects(){ return this.http - .get(`${this.BASE_URL}/s3/objects`) + .get(`${this.BASE_URL}/s3/objects`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -167,7 +181,7 @@ export class AwsService { public getBucketSize(){ return this.http - .get(`${this.BASE_URL}/s3/size`) + .get(`${this.BASE_URL}/s3/size`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -181,7 +195,7 @@ export class AwsService { public getEBS(){ return this.http - .get(`${this.BASE_URL}/ebs`) + .get(`${this.BASE_URL}/ebs`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -195,7 +209,7 @@ export class AwsService { public getRDSInstances(){ return this.http - .get(`${this.BASE_URL}/rds/instances`) + .get(`${this.BASE_URL}/rds/instances`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -209,7 +223,7 @@ export class AwsService { public getDynamoDBTables(){ return this.http - .get(`${this.BASE_URL}/dynamodb/tables`) + .get(`${this.BASE_URL}/dynamodb/tables`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -223,7 +237,7 @@ export class AwsService { public getElasticacheClusters(){ return this.http - .get(`${this.BASE_URL}/elasticache/clusters`) + .get(`${this.BASE_URL}/elasticache/clusters`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -237,7 +251,7 @@ export class AwsService { public getVirtualPrivateClouds(){ return this.http - .get(`${this.BASE_URL}/vpc`) + .get(`${this.BASE_URL}/vpc`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -251,7 +265,7 @@ export class AwsService { public getAccessControlLists(){ return this.http - .get(`${this.BASE_URL}/acl`) + .get(`${this.BASE_URL}/acl`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -265,7 +279,7 @@ export class AwsService { public getRouteTables(){ return this.http - .get(`${this.BASE_URL}/route_tables`) + .get(`${this.BASE_URL}/route_tables`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -279,7 +293,7 @@ export class AwsService { public getCloudFrontRequests(){ return this.http - .get(`${this.BASE_URL}/cloudfront/requests`) + .get(`${this.BASE_URL}/cloudfront/requests`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -293,7 +307,7 @@ export class AwsService { public getCloudFrontDistributions(){ return this.http - .get(`${this.BASE_URL}/cloudfront/distributions`) + .get(`${this.BASE_URL}/cloudfront/distributions`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -307,7 +321,7 @@ export class AwsService { public getApiGatewayRequests(){ return this.http - .get(`${this.BASE_URL}/apigateway/requests`) + .get(`${this.BASE_URL}/apigateway/requests`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -321,7 +335,7 @@ export class AwsService { public getApiGatewayRestAPIs(){ return this.http - .get(`${this.BASE_URL}/apigateway/apis`) + .get(`${this.BASE_URL}/apigateway/apis`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -335,7 +349,7 @@ export class AwsService { public getELBRequests(){ return this.http - .get(`${this.BASE_URL}/elb/requests`) + .get(`${this.BASE_URL}/elb/requests`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -349,7 +363,7 @@ export class AwsService { public getELBFamily(){ return this.http - .get(`${this.BASE_URL}/elb/family`) + .get(`${this.BASE_URL}/elb/family`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -363,7 +377,7 @@ export class AwsService { public getKMSKeys(){ return this.http - .get(`${this.BASE_URL}/kms`) + .get(`${this.BASE_URL}/kms`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -377,7 +391,7 @@ export class AwsService { public getSecurityGroups(){ return this.http - .get(`${this.BASE_URL}/security_groups`) + .get(`${this.BASE_URL}/security_groups`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -391,7 +405,7 @@ export class AwsService { public getKeyPairs(){ return this.http - .get(`${this.BASE_URL}/key_pairs`) + .get(`${this.BASE_URL}/key_pairs`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -405,7 +419,7 @@ export class AwsService { public getACMListCertificates(){ return this.http - .get(`${this.BASE_URL}/acm/certificates`) + .get(`${this.BASE_URL}/acm/certificates`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -419,7 +433,7 @@ export class AwsService { public getACMExpiredCertificates(){ return this.http - .get(`${this.BASE_URL}/acm/expired`) + .get(`${this.BASE_URL}/acm/expired`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -433,7 +447,7 @@ export class AwsService { public getUnrestrictedSecurityGroups(){ return this.http - .get(`${this.BASE_URL}/security_groups/unrestricted`) + .get(`${this.BASE_URL}/security_groups/unrestricted`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -447,7 +461,7 @@ export class AwsService { public getSQSPublishedMessagesMetrics(){ return this.http - .get(`${this.BASE_URL}/sqs/messages`) + .get(`${this.BASE_URL}/sqs/messages`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -461,7 +475,7 @@ export class AwsService { public getSQSQueues(){ return this.http - .get(`${this.BASE_URL}/sqs/queues`) + .get(`${this.BASE_URL}/sqs/queues`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -475,7 +489,7 @@ export class AwsService { public getSNSTopics(){ return this.http - .get(`${this.BASE_URL}/sns/topics`) + .get(`${this.BASE_URL}/sns/topics`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -489,7 +503,7 @@ export class AwsService { public getActiveMQBrokers(){ return this.http - .get(`${this.BASE_URL}/mq/brokers`) + .get(`${this.BASE_URL}/mq/brokers`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -503,7 +517,7 @@ export class AwsService { public getKinesisStreams(){ return this.http - .get(`${this.BASE_URL}/kinesis/streams`) + .get(`${this.BASE_URL}/kinesis/streams`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -517,7 +531,7 @@ export class AwsService { public getKinesisShards(){ return this.http - .get(`${this.BASE_URL}/kinesis/shards`) + .get(`${this.BASE_URL}/kinesis/shards`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -531,7 +545,7 @@ export class AwsService { public getGlueCrawlers(){ return this.http - .get(`${this.BASE_URL}/glue/crawlers`) + .get(`${this.BASE_URL}/glue/crawlers`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -545,7 +559,7 @@ export class AwsService { public getGlueJobs(){ return this.http - .get(`${this.BASE_URL}/glue/jobs`) + .get(`${this.BASE_URL}/glue/jobs`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -559,7 +573,7 @@ export class AwsService { public getDataPipelines(){ return this.http - .get(`${this.BASE_URL}/datapipeline/pipelines`) + .get(`${this.BASE_URL}/datapipeline/pipelines`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -573,7 +587,7 @@ export class AwsService { public getESDomains(){ return this.http - .get(`${this.BASE_URL}/es/domains`) + .get(`${this.BASE_URL}/es/domains`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -587,7 +601,7 @@ export class AwsService { public getSWFDomains(){ return this.http - .get(`${this.BASE_URL}/swf/domains`) + .get(`${this.BASE_URL}/swf/domains`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -601,7 +615,7 @@ export class AwsService { public getOpenSupportTickets(){ return this.http - .get(`${this.BASE_URL}/support/open`) + .get(`${this.BASE_URL}/support/open`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -615,7 +629,7 @@ export class AwsService { public getSupportTicketsHistory(){ return this.http - .get(`${this.BASE_URL}/support/history`) + .get(`${this.BASE_URL}/support/history`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -629,7 +643,7 @@ export class AwsService { public getECS(){ return this.http - .get(`${this.BASE_URL}/ecs`) + .get(`${this.BASE_URL}/ecs`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -643,7 +657,7 @@ export class AwsService { public getRoute53Records(){ return this.http - .get(`${this.BASE_URL}/route53/records`) + .get(`${this.BASE_URL}/route53/records`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -657,7 +671,7 @@ export class AwsService { public getRoute53Zones(){ return this.http - .get(`${this.BASE_URL}/route53/zones`) + .get(`${this.BASE_URL}/route53/zones`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -671,7 +685,7 @@ export class AwsService { public getLogsVolume(){ return this.http - .get(`${this.BASE_URL}/logs/volume`) + .get(`${this.BASE_URL}/logs/volume`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -685,7 +699,7 @@ export class AwsService { public getConsoleLoginEvents(){ return this.http - .get(`${this.BASE_URL}/cloudtrail/sign_in_event`) + .get(`${this.BASE_URL}/cloudtrail/sign_in_event`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -699,7 +713,7 @@ export class AwsService { public getLambdaErrors(){ return this.http - .get(`${this.BASE_URL}/lambda/errors`) + .get(`${this.BASE_URL}/lambda/errors`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -713,7 +727,7 @@ export class AwsService { public getReservedInstances(){ return this.http - .get(`${this.BASE_URL}/ec2/reserved`) + .get(`${this.BASE_URL}/ec2/reserved`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -727,7 +741,7 @@ export class AwsService { public getScheduledInstances(){ return this.http - .get(`${this.BASE_URL}/ec2/scheduled`) + .get(`${this.BASE_URL}/ec2/scheduled`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -741,7 +755,7 @@ export class AwsService { public getSpotInstances(){ return this.http - .get(`${this.BASE_URL}/ec2/spot`) + .get(`${this.BASE_URL}/ec2/spot`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -755,7 +769,7 @@ export class AwsService { public getCostPerInstanceType(){ return this.http - .get(`${this.BASE_URL}/cost/instance_type`) + .get(`${this.BASE_URL}/cost/instance_type`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -769,7 +783,7 @@ export class AwsService { public getEKSClusters(){ return this.http - .get(`${this.BASE_URL}/eks/clusters`) + .get(`${this.BASE_URL}/eks/clusters`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -783,7 +797,7 @@ export class AwsService { public getConsoleLoginSourceIps(){ return this.http - .get(`${this.BASE_URL}/cloudtrail/source_ip`) + .get(`${this.BASE_URL}/cloudtrail/source_ip`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -797,7 +811,7 @@ export class AwsService { public getLogsRetentionPeriod(){ return this.http - .get(`${this.BASE_URL}/logs/retention`) + .get(`${this.BASE_URL}/logs/retention`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -811,7 +825,7 @@ export class AwsService { public getNatGatewayTraffic(){ return this.http - .get(`${this.BASE_URL}/nat/traffic`) + .get(`${this.BASE_URL}/nat/traffic`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -825,7 +839,7 @@ export class AwsService { public getOrganization(){ return this.http - .get(`${this.BASE_URL}/iam/organization`) + .get(`${this.BASE_URL}/iam/organization`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -839,7 +853,7 @@ export class AwsService { public getServiceLimits(){ return this.http - .get(`${this.BASE_URL}/service/limits`) + .get(`${this.BASE_URL}/service/limits`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -853,7 +867,7 @@ export class AwsService { public getEmptyBuckets(){ return this.http - .get(`${this.BASE_URL}/s3/empty`) + .get(`${this.BASE_URL}/s3/empty`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -867,7 +881,7 @@ export class AwsService { public getDetachedElasticIps(){ return this.http - .get(`${this.BASE_URL}/eip/detached`) + .get(`${this.BASE_URL}/eip/detached`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -881,7 +895,7 @@ export class AwsService { public getRedshiftClusters(){ return this.http - .get(`${this.BASE_URL}/redshift/clusters`) + .get(`${this.BASE_URL}/redshift/clusters`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -895,7 +909,7 @@ export class AwsService { public getVPCSubnets(){ return this.http - .get(`${this.BASE_URL}/vpc/subnets`) + .get(`${this.BASE_URL}/vpc/subnets`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -909,7 +923,7 @@ export class AwsService { public getForecastPrice(){ return this.http - .get(`${this.BASE_URL}/cost/forecast`) + .get(`${this.BASE_URL}/cost/forecast`, {headers: this.getHeaders()}) .map(res => { return res.json() }) @@ -920,4 +934,10 @@ export class AwsService { return Observable.throw(err.json().error) }) } + + private getHeaders(){ + let headers = new Headers(); + headers.append('profile', localStorage.getItem('profile')); + return headers; + } } diff --git a/dashboard/src/app/compute/aws/aws.component.ts b/dashboard/src/app/compute/aws/aws.component.ts index a5b2a4915..afc337fbb 100644 --- a/dashboard/src/app/compute/aws/aws.component.ts +++ b/dashboard/src/app/compute/aws/aws.component.ts @@ -1,10 +1,12 @@ -import { Component, OnInit, AfterViewInit } from '@angular/core'; +import { Component, OnInit, AfterViewInit, OnDestroy } from '@angular/core'; import { AwsService } from '../../aws.service'; +import { StoreService } from '../../store.service'; import * as Chartist from 'chartist'; import 'chartist-plugin-tooltips'; declare var Chart: any; declare var Circles: any; import * as $ from "jquery"; +import { Subject, Subscription } from 'rxjs'; declare var moment: any; @Component({ @@ -12,7 +14,13 @@ declare var moment: any; templateUrl: './aws.component.html', styleUrls: ['./aws.component.css'] }) -export class AwsComputeComponent implements OnInit, AfterViewInit { +export class AwsComputeComponent implements OnInit, AfterViewInit, OnDestroy { + private costPerInstanceTypeChart: any; + private lambdaInvocationsChart: any; + private lambdaErrorsChart:any; + private instancesFamilyChart:any; + private instancesPrivacyChart:any; + public runningEC2Instances: number = 0; public stoppedEC2Instances: number = 0; public terminatedEC2Instances: number = 0; @@ -44,9 +52,70 @@ export class AwsComputeComponent implements OnInit, AfterViewInit { public loadingLambdaInvocationsChart: boolean = true; public loadingLambdaErrorsChart: boolean = true; + private _subscription: Subscription; + + + constructor(private awsService: AwsService, private storeService: StoreService) { + this.initState(); + + this._subscription = this.storeService.profileChanged.subscribe(profile => { + this.costPerInstanceTypeChart.detach(); + this.lambdaInvocationsChart.detach(); + this.lambdaErrorsChart.detach(); + this.instancesFamilyChart.destroy(); + this.instancesPrivacyChart.destroy(); + + let tooltips = document.getElementsByClassName('chartist-tooltip') + for (let i = 0; i < tooltips.length; i++) { + tooltips[i].outerHTML = "" + } + for (let j = 0; j < 3; j++) { + let charts = document.getElementsByTagName('svg'); + for (let i = 0; i < charts.length; i++) { + charts[i].outerHTML = "" + } + } + + this.runningEC2Instances = 0; + this.stoppedEC2Instances = 0; + this.terminatedEC2Instances = 0; + this.lambdaFunctions = {}; + this.ecsServices = 0; + this.ecsTasks = 0; + this.ecsClusters = 0; + this.reservedInstances = 0; + this.spotInstances = 0; + this.scheduledInstances = 0; + this.eksClusters = 0; + this.detchedElasticIps = 0; + + this.loadingRunningInstances = true; + this.loadingStoppedInstances = true; + this.loadingTerminatedInstances = true; + this.loadingReservedInstances = true; + this.loadingSpotInstances = true; + this.loadingScheduledInstances = true; + this.loadingDetachedIps = true; + this.loadingLambdaFunctions= true; + this.loadingEcsClusters= true; + this.loadingEcsTasks = true; + this.loadingEcsServices = true; + this.loadingEksClusters= true; + this.loadingInstancesPrivacyChart = true; + this.loadingInstancesFamilyChart= true; + this.loadingCostPerInstanceTypeChart= true; + this.loadingLambdaInvocationsChart= true; + this.loadingLambdaErrorsChart= true; + + this.initState(); + }); + } - constructor(private awsService: AwsService) { + ngOnDestroy() { + this._subscription.unsubscribe(); + } + private initState(){ this.lambdaFunctions = {} this.awsService.getDetachedElasticIps().subscribe(data => { @@ -141,7 +210,6 @@ export class AwsComputeComponent implements OnInit, AfterViewInit { this.showLambdaInvocations(labels, series); }, err => { this.loadingLambdaInvocationsChart = false; - console.log(err) }); this.awsService.getECS().subscribe(data => { @@ -188,7 +256,6 @@ export class AwsComputeComponent implements OnInit, AfterViewInit { this.showLambdaErrors(labels, series); }, err => { this.loadingLambdaErrorsChart = false; - console.log(err); }); this.awsService.getReservedInstances().subscribe(data => { @@ -252,7 +319,6 @@ export class AwsComputeComponent implements OnInit, AfterViewInit { this.showCostPerInstanceType(periods, series); }, err => { this.loadingCostPerInstanceTypeChart = false; - console.log(err); }); } @@ -303,14 +369,14 @@ export class AwsComputeComponent implements OnInit, AfterViewInit { height: "245px", } - new Chartist.Bar('.costPerInstanceTypeChart', costHistory, optionChartCostHistory); + this.costPerInstanceTypeChart = new Chartist.Bar('.costPerInstanceTypeChart', costHistory, optionChartCostHistory); } private showInstancesPrivacy(series){ var canvas : any = document.getElementById('instancesPrivacyChart'); var ctx = canvas.getContext('2d'); - new Chart(ctx, { + this.instancesPrivacyChart = new Chart(ctx, { type: 'pie', data: { datasets: [{ @@ -325,7 +391,7 @@ export class AwsComputeComponent implements OnInit, AfterViewInit { private showLambdaErrors(labels, series){ let scope = this; - new Chartist.Line('.lambdaErrorsChart', { + this.lambdaErrorsChart = new Chartist.Line('.lambdaErrorsChart', { labels: labels, series: series }, { @@ -349,7 +415,7 @@ export class AwsComputeComponent implements OnInit, AfterViewInit { private showLambdaInvocations(labels, series){ let scope = this; - new Chartist.Bar('.lambdaInvocationsChart', { + this.lambdaInvocationsChart = new Chartist.Bar('.lambdaInvocationsChart', { labels: labels, series: series }, { @@ -415,7 +481,7 @@ export class AwsComputeComponent implements OnInit, AfterViewInit { }; var ctx = document.getElementById('instancesFamilyChart'); - var chart = new Chart.PolarArea(ctx, config); + this.instancesFamilyChart = new Chart.PolarArea(ctx, config); } } diff --git a/dashboard/src/app/dashboard/aws/aws.component.ts b/dashboard/src/app/dashboard/aws/aws.component.ts index facf2b1db..51c1c3e1f 100644 --- a/dashboard/src/app/dashboard/aws/aws.component.ts +++ b/dashboard/src/app/dashboard/aws/aws.component.ts @@ -1,5 +1,7 @@ -import { Component, OnInit, AfterViewInit } from '@angular/core'; +import { Component, OnInit, AfterViewInit, OnDestroy } from '@angular/core'; import { AwsService } from '../../aws.service'; +import { StoreService } from '../../store.service'; +import { Subject, Subscription } from 'rxjs'; import * as Chartist from 'chartist'; import 'chartist-plugin-tooltips'; import 'jquery-mapael'; @@ -12,7 +14,7 @@ declare var Chart: any; templateUrl: './aws.component.html', styleUrls: ['./aws.component.css'] }) -export class AwsDashboardComponent implements OnInit, AfterViewInit { +export class AwsDashboardComponent implements OnInit, AfterViewInit, OnDestroy { public iamUsers: number = 0; public currentBill: number = 0; public usedRegions: number = 0; @@ -103,9 +105,41 @@ export class AwsDashboardComponent implements OnInit, AfterViewInit { latitude: -23.5505199, longitude: -46.6333094 } + }; + + private _subscription: Subscription; + + constructor(private AwsService: AwsService, private storeService: StoreService) { + this.initState(); + + this._subscription = this.storeService.profileChanged.subscribe(profile => { + this.iamUsers = 0; + this.currentBill = 0; + this.usedRegions = 0; + this.redAlarms = 0; + this.mostUsedServices = []; + this.openTickets = 0; + this.resolvedTickets = 0; + this.forecastBill = '0'; + + this.loadingCurrentBill = true; + this.loadingIamUsers = true; + this.loadingUsedRegions = true; + this.loadingRedAlarms = true; + this.loadingOpenTickets = true; + this.loadingResolvedTickets = true; + this.loadingCostHistoryChart = true; + this.loadingForecastBill = true; + + this.initState(); + }) } - constructor(private AwsService: AwsService) { + ngOnDestroy() { + this._subscription.unsubscribe(); + } + + private initState() { this.mostUsedServices = [] this.AwsService.getIAMUsers().subscribe(data => { @@ -217,12 +251,12 @@ export class AwsDashboardComponent implements OnInit, AfterViewInit { }); } - ngOnInit() {} + ngOnInit() { } ngAfterViewInit(): void { this.showEC2InstancesPerRegion({}); } - + public formatNumber(labelValue) { // Nine Zeroes for Billions @@ -243,7 +277,7 @@ export class AwsDashboardComponent implements OnInit, AfterViewInit { } public showEC2InstancesPerRegion(plots) { - var canvas : any = $(".mapregions"); + var canvas: any = $(".mapregions"); canvas.mapael({ map: { name: "world_countries", @@ -332,7 +366,7 @@ export class AwsDashboardComponent implements OnInit, AfterViewInit { }, axisY: { offset: 80, - labelInterpolationFnc: function(value) { + labelInterpolationFnc: function (value) { return scope.formatNumber(value) }, }, diff --git a/dashboard/src/app/data-and-ai/aws/aws.component.ts b/dashboard/src/app/data-and-ai/aws/aws.component.ts index 592ab9995..5f97488be 100644 --- a/dashboard/src/app/data-and-ai/aws/aws.component.ts +++ b/dashboard/src/app/data-and-ai/aws/aws.component.ts @@ -1,6 +1,7 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { AwsService } from '../../aws.service'; - +import { StoreService } from '../../store.service'; +import { Subject, Subscription } from 'rxjs'; declare var Chart: any; declare var $: any; declare var window: any; @@ -14,7 +15,8 @@ import 'chartist-plugin-tooltips'; templateUrl: './aws.component.html', styleUrls: ['./aws.component.css'] }) -export class AwsDataAndAIComponent implements OnInit { +export class AwsDataAndAIComponent implements OnInit, OnDestroy { + private sqsMessagesChart:any; public sqsQueues: number = 0; public numberOfMessagesSentToday: number = 0; @@ -42,7 +44,60 @@ export class AwsDataAndAIComponent implements OnInit { public loadingESDomains: boolean = true; public loadingSQSMessagesChart: boolean = true; - constructor(private awsService: AwsService) { + private _subscription: Subscription; + + constructor(private awsService: AwsService, private storeService: StoreService) { + this.initState(); + + this._subscription = this.storeService.profileChanged.subscribe(profile => { + this.sqsMessagesChart.detach(); + + let tooltips = document.getElementsByClassName('chartist-tooltip') + for (let i = 0; i < tooltips.length; i++) { + tooltips[i].outerHTML = "" + } + for (let j = 0; j < 3; j++) { + let charts = document.getElementsByTagName('svg'); + for (let i = 0; i < charts.length; i++) { + charts[i].outerHTML = "" + } + } + + this.sqsQueues = 0; + this.numberOfMessagesSentToday = 0; + this.numberOfMessagesDeletedToday = 0; + this.snsTopics = 0; + this.activemqBrokers = 0; + this.kinesisStreams = 0; + this.kinesisShards = 0; + this.glueJobs = 0; + this.glueCrawlers = 0; + this.dataPipelines = 0; + this.esDomains = 0; + this.swfDomains = 0; + + this.loadingSQS = true; + this.loadingSQSMessages = true; + this.loadingSNS = true; + this.loadingGlueCrawlers = true; + this.loadingActiveMQBrokers = true; + this.loadingGlueJobs = true; + this.loadingSwfDomains = true; + this.loadingDataPipelines = true; + this.loadingKinesisStreams = true; + this.loadingKinesisShards = true; + this.loadingESDomains = true; + this.loadingSQSMessagesChart = true; + + this.initState(); + }) + } + + ngOnDestroy() { + this._subscription.unsubscribe(); + } + + private initState(){ this.awsService.getSQSPublishedMessagesMetrics().subscribe(data => { this.numberOfMessagesSentToday = data[0].Datapoints[Object.keys(data[0].Datapoints)[Object.keys(data[0].Datapoints).length - 1]] this.numberOfMessagesDeletedToday = data[1].Datapoints[Object.keys(data[1].Datapoints)[Object.keys(data[1].Datapoints).length - 1]] @@ -74,7 +129,6 @@ export class AwsDataAndAIComponent implements OnInit { }, err => { this.loadingSQSMessagesChart = false; this.loadingSQSMessages = false; - console.log(err); }) this.awsService.getSQSQueues().subscribe(data => { @@ -181,7 +235,7 @@ export class AwsDataAndAIComponent implements OnInit { private showSQSMessages(labels, series) { let scope = this; - new Chartist.Bar('#sqsMessagesChart', { + this.sqsMessagesChart = new Chartist.Bar('#sqsMessagesChart', { labels: labels, series: series }, { diff --git a/dashboard/src/app/limits/aws/aws.component.ts b/dashboard/src/app/limits/aws/aws.component.ts index bf665086c..2232d752b 100644 --- a/dashboard/src/app/limits/aws/aws.component.ts +++ b/dashboard/src/app/limits/aws/aws.component.ts @@ -1,18 +1,34 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit,OnDestroy } from '@angular/core'; import { AwsService } from '../../aws.service'; +import { StoreService } from '../../store.service'; +import { Subject, Subscription } from 'rxjs'; @Component({ selector: 'aws-limits', templateUrl: './aws.component.html', styleUrls: ['./aws.component.css'] }) -export class AwsLimitsComponent implements OnInit { +export class AwsLimitsComponent implements OnInit,OnDestroy { public serviceLimits: Array = []; public loadingServiceLimits: boolean = true; - constructor(private awsService: AwsService) { + private _subscription: Subscription; + + constructor(private awsService: AwsService, private storeService: StoreService) { + this.initState(); + + this._subscription = this.storeService.profileChanged.subscribe(profile => { + this.serviceLimits = []; + + this.loadingServiceLimits = true; + + this.initState(); + }); + } + + private initState(){ this.awsService.getServiceLimits().subscribe(data => { this.serviceLimits = data; this.loadingServiceLimits = false; @@ -22,6 +38,10 @@ export class AwsLimitsComponent implements OnInit { }); } + ngOnDestroy() { + this._subscription.unsubscribe(); + } + public getColor(status: string) { switch (status) { case 'ok': diff --git a/dashboard/src/app/network/aws/aws.component.ts b/dashboard/src/app/network/aws/aws.component.ts index 53dfa3068..592d3a601 100644 --- a/dashboard/src/app/network/aws/aws.component.ts +++ b/dashboard/src/app/network/aws/aws.component.ts @@ -1,5 +1,7 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { AwsService } from '../../aws.service'; +import { StoreService } from '../../store.service'; +import { Subject, Subscription } from 'rxjs'; declare var Chart: any; declare var $: any; declare var window: any; @@ -13,7 +15,13 @@ import 'chartist-plugin-tooltips'; templateUrl: './aws.component.html', styleUrls: ['./aws.component.css'] }) -export class AwsNetworkComponent implements OnInit { +export class AwsNetworkComponent implements OnInit, OnDestroy { + private cloudfrontRequests:any; + private apigatewayRequests:any; + private elbRequests: any; + private elbFamilyType:any; + private natGatewayChartTraffic:any; + public vpcNumber: number; public aclNumber: number; public subnetNumbers: number; @@ -50,7 +58,74 @@ export class AwsNetworkComponent implements OnInit { public loadingNatGatewayTrafficChart: boolean = true; public loadingElbFamilyType: boolean = true; - constructor(private awsService: AwsService) { + private _subscription: Subscription; + + constructor(private awsService: AwsService, private storeService: StoreService) { + this.initState(); + + this._subscription = this.storeService.profileChanged.subscribe(profile => { + this.cloudfrontRequests.destroy(); + this.apigatewayRequests.destroy(); + this.elbRequests.destroy(); + this.elbFamilyType.destroy(); + //this.natGatewayChartTraffic.detach(); + + let tooltips = document.getElementsByClassName('chartist-tooltip') + for (let i = 0; i < tooltips.length; i++) { + tooltips[i].outerHTML = "" + } + for (let j = 0; j < 3; j++) { + let charts = document.getElementsByTagName('svg'); + for (let i = 0; i < charts.length; i++) { + charts[i].outerHTML = "" + } + } + + this.vpcNumber = 0; + this.aclNumber = 0; + this.subnetNumbers = 0; + this.routeTablesNumber = 0; + this.cloudfrontDistributions = 0; + this.cdnYesterdayRequests = '0'; + this.cdnTodayRequests = '0'; + this.apigatewayYesterdayRequests = '0'; + this.apigatewayTodayRequests = '0'; + this.apigatewayApis = 0; + this.elbYesterdayRequests = '0'; + this.elbTodayRequests = '0'; + this.loadBalancers = 0; + this.route53Records = 0; + this.route53Zones = 0; + this.natGatewayAvailableRegions = []; + this.natGatewayTraffic = []; + + this.loadingVPCNumbers= true; + this.loadingACLNumbers = true; + this.loadingSubnetNumbers = true; + this.loadingRouteTablesNumber = true; + this.loadingCDNNumbers = true; + this.loadingCDNRequests = true; + this.loadingAPIGateways = true; + this.loadingAPIRequests = true; + this.loadingELBNumber = true; + this.loadingElbRequests = true; + this.loadingRoute53Zones = true; + this.loadingRoute53ARecords = true; + this.loadingCloudfrontRequestsChart = true; + this.loadingApigatewayRequestsChart = true; + this.loadingElbRequestsChart = true; + this.loadingNatGatewayTrafficChart = true; + this.loadingElbFamilyType = true; + + this.initState(); + }); + } + + ngOnDestroy() { + this._subscription.unsubscribe(); + } + + private initState() { this.awsService.getVirtualPrivateClouds().subscribe(data => { this.vpcNumber = data; this.loadingVPCNumbers = false; @@ -348,7 +423,7 @@ export class AwsNetworkComponent implements OnInit { private showNatGatewayTraffic(labels, series) { let scope = this; - new Chartist.Bar('#natGatewayChartTraffic', { + this.natGatewayChartTraffic = new Chartist.Bar('#natGatewayChartTraffic', { labels: labels, series: series }, { @@ -416,9 +491,9 @@ export class AwsNetworkComponent implements OnInit { }; - var canvas : any = document.getElementById('elbFamilyType'); + var canvas: any = document.getElementById('elbFamilyType'); var ctx = canvas.getContext('2d'); - window.myBar = new Chart(ctx, { + this.elbFamilyType = new Chart(ctx, { type: 'pie', data: barChartData, options: { @@ -499,9 +574,9 @@ export class AwsNetworkComponent implements OnInit { } }; - var canvas : any = document.getElementById('apigatewayRequests'); + var canvas: any = document.getElementById('apigatewayRequests'); var ctx = canvas.getContext('2d'); - new Chart(ctx, config); + this.apigatewayRequests = new Chart(ctx, config); } private showCloudFrontRequests(datasets) { @@ -554,9 +629,9 @@ export class AwsNetworkComponent implements OnInit { } }; - var canvas : any = document.getElementById('cloudfrontRequests'); + var canvas: any = document.getElementById('cloudfrontRequests'); var ctx = canvas.getContext('2d'); - new Chart(ctx, config); + this.cloudfrontRequests = new Chart(ctx, config); } private showELBRequests(datasets) { @@ -609,9 +684,9 @@ export class AwsNetworkComponent implements OnInit { } }; - var canvas : any = document.getElementById('elbRequests'); + var canvas: any = document.getElementById('elbRequests'); var ctx = canvas.getContext('2d'); - new Chart(ctx, config); + this.elbRequests = new Chart(ctx, config); } } diff --git a/dashboard/src/app/notifications/notifications.component.css b/dashboard/src/app/notifications/notifications.component.css new file mode 100644 index 000000000..4a6594722 --- /dev/null +++ b/dashboard/src/app/notifications/notifications.component.css @@ -0,0 +1,3 @@ +.badge-margin{ + margin-right: 20px; +} \ No newline at end of file diff --git a/dashboard/src/app/notifications/notifications.component.html b/dashboard/src/app/notifications/notifications.component.html new file mode 100644 index 000000000..e231226af --- /dev/null +++ b/dashboard/src/app/notifications/notifications.component.html @@ -0,0 +1,18 @@ +
    +
    +

    Notifications

    + +
    +
    + +
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/dashboard/src/app/notifications/notifications.component.spec.ts b/dashboard/src/app/notifications/notifications.component.spec.ts new file mode 100644 index 000000000..0147b0d89 --- /dev/null +++ b/dashboard/src/app/notifications/notifications.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NotificationsComponent } from './notifications.component'; + +describe('NotificationsComponent', () => { + let component: NotificationsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NotificationsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NotificationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/dashboard/src/app/notifications/notifications.component.ts b/dashboard/src/app/notifications/notifications.component.ts new file mode 100644 index 000000000..60a5fd4e8 --- /dev/null +++ b/dashboard/src/app/notifications/notifications.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { StoreService } from '../store.service'; +import { Subscription } from 'rxjs'; +import * as moment from 'moment'; + +@Component({ + selector: 'app-notifications', + templateUrl: './notifications.component.html', + styleUrls: ['./notifications.component.css'] +}) +export class NotificationsComponent implements OnInit, OnDestroy { + public notifications: Array = []; + public _subscription: Subscription; + + constructor(private storeService: StoreService) { + + let temp = this.storeService.list(); + Object.keys(temp).forEach(key => { + this.notifications.push(temp[key]); + }) + + this._subscription = this.storeService.newNotification.subscribe(notifications => { + this.notifications = []; + Object.keys(notifications).forEach(key => { + this.notifications.push(notifications[key]); + }) + }) + } + + public calcMoment(timestamp) { + return moment(timestamp).fromNow(); + } + + ngOnDestroy(){ + this._subscription.unsubscribe(); + } + + ngOnInit() { + } + +} diff --git a/dashboard/src/app/profile/aws/aws.component.ts b/dashboard/src/app/profile/aws/aws.component.ts index 25023a997..a2435e491 100644 --- a/dashboard/src/app/profile/aws/aws.component.ts +++ b/dashboard/src/app/profile/aws/aws.component.ts @@ -1,16 +1,31 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { AwsService } from '../../aws.service'; +import { StoreService } from '../../store.service'; +import { Subject, Subscription } from 'rxjs'; @Component({ selector: 'aws-profile', templateUrl: './aws.component.html', styleUrls: ['./aws.component.css'] }) -export class AwsProfileComponent implements OnInit { +export class AwsProfileComponent implements OnInit, OnDestroy { public account : Object = {}; public organization: Object = {}; - constructor(private awsService: AwsService) { + private _subscription: Subscription; + + constructor(private awsService: AwsService, private storeService: StoreService) { + this.initState(); + + this._subscription = this.storeService.profileChanged.subscribe(profile => { + this.account = {}; + this.organization = {}; + + this.initState(); + }); + } + + private initState(){ this.awsService.getAccountName().subscribe(data => { this.account = data; }, err => { @@ -24,6 +39,10 @@ export class AwsProfileComponent implements OnInit { }); } + ngOnDestroy() { + this._subscription.unsubscribe(); + } + ngOnInit() { } diff --git a/dashboard/src/app/security/aws/aws.component.ts b/dashboard/src/app/security/aws/aws.component.ts index 70624fcfc..0443dc4c2 100644 --- a/dashboard/src/app/security/aws/aws.component.ts +++ b/dashboard/src/app/security/aws/aws.component.ts @@ -1,7 +1,8 @@ -import { Component, OnInit, AfterViewInit } from '@angular/core'; +import { Component, OnInit, AfterViewInit, OnDestroy } from '@angular/core'; import { AwsService } from '../../aws.service'; import { PageChangedEvent } from 'ngx-bootstrap/pagination'; - +import { StoreService } from '../../store.service'; +import { Subject, Subscription } from 'rxjs'; import 'jquery-mapael'; import 'jquery-mapael/js/maps/world_countries.js'; import * as $ from "jquery"; @@ -13,7 +14,8 @@ import 'chartist-plugin-tooltips'; templateUrl: './aws.component.html', styleUrls: ['./aws.component.css'] }) -export class AwsSecurityComponent implements OnInit, AfterViewInit { +export class AwsSecurityComponent implements OnInit, AfterViewInit, OnDestroy { + private signInEventsChart:any; public kmsKeys: number; public securityGroups: number; @@ -33,7 +35,44 @@ export class AwsSecurityComponent implements OnInit, AfterViewInit { public loadingACMExpiredCertificates: boolean = true; public loadingSignInEventsChart: boolean = true; - constructor(private awsService: AwsService) { + private _subscription: Subscription; + + constructor(private awsService: AwsService, private storeService: StoreService) { + this.initState(); + + this._subscription = this.storeService.profileChanged.subscribe(profile => { + this.signInEventsChart.detach(); + + for (let j = 0; j < 3; j++) { + let charts = document.getElementsByTagName('svg'); + for (let i = 0; i < charts.length; i++) { + charts[i].outerHTML = "" + } + } + + this.kmsKeys = 0; + this.securityGroups = 0; + this.keyPairs = 0; + this.routeTables = 0; + this.acmCertificates = 0; + this.acmExpiredCertificates = 0; + this.unrestrictedSecurityGroups = []; + this.returnedUnrestrictedSecurityGroups = []; + this.consoleLoginSourceIps = []; + + this.loadingKMSKeys = true; + this.loadingSecurityGroups = true; + this.loadingKeyPairs = true; + this.loadingRouteTables = true; + this.loadingACMCertificates = true; + this.loadingACMExpiredCertificates = true; + this.loadingSignInEventsChart = true; + + this.initState(); + }); + } + + private initState() { this.awsService.getKMSKeys().subscribe(data => { this.kmsKeys = data; this.loadingKMSKeys = false; @@ -144,6 +183,10 @@ export class AwsSecurityComponent implements OnInit, AfterViewInit { }); } + ngOnDestroy() { + this._subscription.unsubscribe(); + } + pageChanged(event: PageChangedEvent): void { const startItem = (event.page - 1) * event.itemsPerPage; const endItem = event.page * event.itemsPerPage; @@ -158,7 +201,7 @@ export class AwsSecurityComponent implements OnInit, AfterViewInit { ngOnInit() { } private showSourceIpLogin(plots) { - var canvas : any = $("#sourceIpsChart"); + var canvas: any = $("#sourceIpsChart"); canvas.mapael({ map: { name: "world_countries", @@ -231,7 +274,7 @@ export class AwsSecurityComponent implements OnInit, AfterViewInit { } private showSignInEvents(labels, series) { - new Chartist.Bar('#signInEventsChart', { + this.signInEventsChart = new Chartist.Bar('#signInEventsChart', { labels: labels, series: series }, { diff --git a/dashboard/src/app/storage/aws/aws.component.ts b/dashboard/src/app/storage/aws/aws.component.ts index d58379d54..2ed39c9e5 100644 --- a/dashboard/src/app/storage/aws/aws.component.ts +++ b/dashboard/src/app/storage/aws/aws.component.ts @@ -1,6 +1,7 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { AwsService } from '../../aws.service'; - +import { StoreService } from '../../store.service'; +import { Subject, Subscription } from 'rxjs'; declare var Chart: any; declare var $: any; declare var window: any; @@ -13,7 +14,12 @@ import 'chartist-plugin-tooltips'; templateUrl: './aws.component.html', styleUrls: ['./aws.component.css'] }) -export class AwsStorageComponent implements OnInit { +export class AwsStorageComponent implements OnInit, OnDestroy { + private s3BucketsSizeChart: any; + private s3BucketsObjectsChart: any; + private ebsFamilyChart: any; + private logsVolumeChart: any; + public s3Buckets: number; public emptyBuckets: number; public s3BucketSize: string; @@ -48,7 +54,72 @@ export class AwsStorageComponent implements OnInit { public loadingEbsFamilyChart: boolean = true; public loadingLogsVolumeChart: boolean = true; - constructor(private awsService: AwsService) { + private _subscription: Subscription; + + constructor(private awsService: AwsService, private storeService: StoreService) { + this.initState(); + + this._subscription = this.storeService.profileChanged.subscribe(profile => { + this.s3BucketsSizeChart.detach(); + this.s3BucketsObjectsChart.detach(); + this.logsVolumeChart.detach(); + this.ebsFamilyChart.destroy(); + + let tooltips = document.getElementsByClassName('chartist-tooltip') + for (let i = 0; i < tooltips.length; i++) { + tooltips[i].outerHTML = "" + } + for (let j = 0; j < 3; j++) { + let charts = document.getElementsByTagName('svg'); + for (let i = 0; i < charts.length; i++) { + charts[i].outerHTML = "" + } + } + + + this.s3Buckets = 0; + this.emptyBuckets = 0; + this.s3BucketSize = '0 KB'; + this.s3BucketObjects = '0'; + this.ebsTotal = 0; + this.ebsTotalSize = '0 KB'; + this.ebsUsed = 0; + this.dynamodbTables = 0; + this.rdsInstances = 0; + this.docdbInstances = 0; + this.memcachedClusters = 0; + this.redisClusters = 0; + this.logsRetentionPeriod = 0; + this.redshiftClusters = 0; + + this.loadingS3Buckets = true; + this.loadingS3BucketSize = true; + this.loadingS3BucketObjects = true; + this.loadingEmptyBuckets = true; + this.loadingEbsTotal = true; + this.loadingEbsTotalSize = true; + this.loadingEbsUsed = true; + this.loadingLogsRetentionPeriod = true; + this.loadingDynamoTables = true; + this.loadingRdsInstances = true; + this.loadingDocDbInstances = true; + this.loadingRedshiftClusters = true; + this.loadingMemCachedClusters = true; + this.loadingRedisClusters = true; + this.loadingS3BucketsSizeChart = true; + this.loadingS3BucketsObjectsChart = true; + this.loadingEbsFamilyChart = true; + this.loadingLogsVolumeChart = true; + + this.initState(); + }); + } + + ngOnDestroy() { + this._subscription.unsubscribe(); + } + + private initState() { this.awsService.getNumberOfS3Buckets().subscribe(data => { this.s3Buckets = data ? data : 0; this.loadingS3Buckets = false; @@ -68,7 +139,7 @@ export class AwsStorageComponent implements OnInit { }) this.ebsTotal = sum; this.loadingEbsTotal = false; - this.ebsTotalSize = this.bytesToSizeWithUnit(data.total*1024*1024); + this.ebsTotalSize = this.bytesToSizeWithUnit(data.total * 1024 * 1024); this.loadingEbsTotalSize = false; this.ebsUsed = data.state['in-use']; this.loadingEbsUsed = false; @@ -99,7 +170,7 @@ export class AwsStorageComponent implements OnInit { serie.push({ meta: region, value: data[region][timestamp] }) - if (i==0){ + if (i == 0) { labels.push(timestamp) } }) @@ -130,7 +201,7 @@ export class AwsStorageComponent implements OnInit { serie.push({ meta: region, value: data[region][timestamp] }) - if (i==0){ + if (i == 0) { labels.push(timestamp) } }) @@ -143,11 +214,9 @@ export class AwsStorageComponent implements OnInit { total += data[region][Object.keys(data[region])[Object.keys(data[region]).length - 1]] }); - console.log(labels) - console.log(series); this.loadingS3BucketsSizeChart = false; this.loadingS3BucketSize = false; - this.s3BucketSize = this.bytesToSizeWithUnit(total); + this.s3BucketSize = this.bytesToSizeWithUnit(total); this.showS3BucketsSize(labels, series); }, err => { this.s3BucketObjects = '0'; @@ -225,7 +294,6 @@ export class AwsStorageComponent implements OnInit { ]); }, err => { this.loadingLogsVolumeChart = false; - console.log(err); }); this.awsService.getLogsRetentionPeriod().subscribe(data => { @@ -245,10 +313,10 @@ export class AwsStorageComponent implements OnInit { }) } - + private showS3BucketsObjects(labels, series) { let scope = this; - new Chartist.Bar('#s3BucketsObjectsChart', { + this.s3BucketsObjectsChart = new Chartist.Bar('#s3BucketsObjectsChart', { labels: labels, series: series }, { @@ -273,7 +341,7 @@ export class AwsStorageComponent implements OnInit { private showS3BucketsSize(labels, series) { let scope = this; - new Chartist.Bar('#s3BucketsSizeChart', { + this.s3BucketsSizeChart = new Chartist.Bar('#s3BucketsSizeChart', { labels: labels, series: series }, { @@ -298,7 +366,7 @@ export class AwsStorageComponent implements OnInit { private showLogsVolume(labels, series) { let scope = this; - new Chartist.Bar('#logsVolumeChart', { + this.logsVolumeChart = new Chartist.Bar('#logsVolumeChart', { labels: labels, series: series }, { @@ -321,7 +389,7 @@ export class AwsStorageComponent implements OnInit { }); } - ngOnInit() {} + ngOnInit() { } private showEBSFamily(labels, series) { var barChartData = { @@ -339,9 +407,9 @@ export class AwsStorageComponent implements OnInit { }; - var canvas : any = document.getElementById('ebsFamilyChart'); + let canvas: any = document.getElementById('ebsFamilyChart'); var ctx = canvas.getContext('2d'); - new Chart(ctx, { + this.ebsFamilyChart = new Chart(ctx, { type: 'pie', data: barChartData, options: { diff --git a/dashboard/src/app/store.service.ts b/dashboard/src/app/store.service.ts index c5170f61f..8c89c22de 100644 --- a/dashboard/src/app/store.service.ts +++ b/dashboard/src/app/store.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core'; import { Subject } from "rxjs/Subject"; - @Injectable() export class StoreService { @@ -12,6 +11,8 @@ export class StoreService { public providerChanged: Subject = new Subject(); + public profileChanged: Subject = new Subject(); + constructor() { if(localStorage.getItem('provider')){ this.provider = localStorage.getItem('provider'); @@ -48,6 +49,11 @@ export class StoreService { return this.notifications; } + public cleanNotifications(){ + this.notifications = new Map(); + this.newNotification.next(this.notifications); + } + public onProviderChanged(provider: string){ this.provider = provider; localStorage.setItem('provider', this.provider); @@ -56,4 +62,10 @@ export class StoreService { this.newNotification.next(this.notifications); } + public onProfileChanged(profile: string){ + this.profileChanged.next(profile); + this.notifications = new Map(); + this.newNotification.next(this.notifications); + } + } diff --git a/handlers/aws/acm_handler.go b/handlers/aws/acm_handler.go index 542ed97a0..ae6d1f38b 100644 --- a/handlers/aws/acm_handler.go +++ b/handlers/aws/acm_handler.go @@ -1,34 +1,61 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) APIGatewayListCertificatesHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_acm_certificates") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.acm.certificates", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.ListCertificates(handler.cfg) + response, err := handler.aws.ListCertificates(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "acm:ListCertificates is missing") } else { - handler.cache.Set("aws_acm_certificates", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) APIGatewayExpiredCertificatesHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_acm_expired") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.acm.expired", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.ListExpiredCertificates(handler.cfg) + response, err := handler.aws.ListExpiredCertificates(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "acm:ListCertificates is missing") } else { - handler.cache.Set("aws_acm_expired", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/activemq_handler.go b/handlers/aws/activemq_handler.go index beabb6520..493fbe4a4 100644 --- a/handlers/aws/activemq_handler.go +++ b/handlers/aws/activemq_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) ActiveMQBrokersHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_mq_brokers") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.mq.brokers", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.ListBrokers(handler.cfg) + response, err := handler.aws.ListBrokers(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "mq:ListBrokers is missing") } else { - handler.cache.Set("aws_mq_brokers", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/apigateway_handler.go b/handlers/aws/apigateway_handler.go index df6515231..07449e549 100644 --- a/handlers/aws/apigateway_handler.go +++ b/handlers/aws/apigateway_handler.go @@ -1,34 +1,61 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) APIGatewayRequestsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_apigateway_requests") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.apigateway.requests", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetAPIGatewayRequests(handler.cfg) + response, err := handler.aws.GetAPIGatewayRequests(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:GetMetricStatistics is missing") } else { - handler.cache.Set("aws_apigateway_requests", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) APIGatewayRestAPIsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_apigateway_apis") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.apigateway.apis", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetRestAPIs(handler.cfg) + response, err := handler.aws.GetRestAPIs(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "apigateway:GET is missing") } else { - handler.cache.Set("aws_apigateway_apis", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/cloudfront_handler.go b/handlers/aws/cloudfront_handler.go index dbb1e0784..20a37671c 100644 --- a/handlers/aws/cloudfront_handler.go +++ b/handlers/aws/cloudfront_handler.go @@ -1,34 +1,61 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) CloudFrontDistributionsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_cloudfront") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.cloudfront.distributions", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeCloudFrontDistributions(handler.cfg) + response, err := handler.aws.DescribeCloudFrontDistributions(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudfront:ListDistributions is missing") } else { - handler.cache.Set("aws_cloudfront", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) CloudFrontRequestsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_cloudfront_requests") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.cloudfront.requests", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetCloudFrontRequests(handler.cfg) + response, err := handler.aws.GetCloudFrontRequests(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:GetMetricStatistics is missing") } else { - handler.cache.Set("aws_cloudfront_requests", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/cloudtrail_handler.go b/handlers/aws/cloudtrail_handler.go index 6f5e4ef22..c8a85b931 100644 --- a/handlers/aws/cloudtrail_handler.go +++ b/handlers/aws/cloudtrail_handler.go @@ -1,34 +1,61 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) CloudTrailConsoleSignInEventsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_events_sign_in") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.cloudtrail.signin", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.CloudTrailConsoleSignInEvents(handler.cfg) + response, err := handler.aws.CloudTrailConsoleSignInEvents(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudtrail:LookupEvents is missing") } else { - handler.cache.Set("aws_events_sign_in", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) CloudTrailConsoleSignInSourceIpEventsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_events_source_ip") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.cloudtrail.sourceip", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.CloudTrailConsoleSignInSourceIpEvents(handler.cfg) + response, err := handler.aws.CloudTrailConsoleSignInSourceIpEvents(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudtrail:LookupEvents is missing") } else { - handler.cache.Set("aws_events_source_ip", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/cloudwatch_handler.go b/handlers/aws/cloudwatch_handler.go index 990eaf285..b6f1b6647 100644 --- a/handlers/aws/cloudwatch_handler.go +++ b/handlers/aws/cloudwatch_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) CloudWatchAlarmsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_cloudwatch_alarms") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.cloudwatch.alarms", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeCloudWatchAlarms(handler.cfg) + response, err := handler.aws.DescribeCloudWatchAlarms(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:DescribeAlarms is missing") } else { - handler.cache.Set("aws_cloudwatch_alarms", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/config_handler.go b/handlers/aws/config_handler.go new file mode 100644 index 000000000..2ea31d848 --- /dev/null +++ b/handlers/aws/config_handler.go @@ -0,0 +1,21 @@ +package aws + +import ( + "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" + . "github.com/mlabouardy/komiser/services/ini" +) + +func (handler *AWSHandler) ConfigProfilesHandler(w http.ResponseWriter, r *http.Request) { + if handler.multiple { + sections, err := OpenFile(external.DefaultSharedCredentialsFilename()) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't parse credentials file") + } + respondWithJSON(w, 200, sections.List()) + } else { + respondWithJSON(w, 200, []string{}) + } + +} diff --git a/handlers/aws/costexplorer_handler.go b/handlers/aws/costexplorer_handler.go index dfe5c9bc1..6c8d3fcb6 100644 --- a/handlers/aws/costexplorer_handler.go +++ b/handlers/aws/costexplorer_handler.go @@ -1,64 +1,115 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) CostAndUsageHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("cost_usage_history") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ce.history", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeCostAndUsage(handler.cfg) + response, err := handler.aws.DescribeCostAndUsage(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ce:GetCostAndUsage is missing") } else { - handler.cache.Set("cost_usage_history", response.History) + handler.cache.Set(key, response.History) respondWithJSON(w, 200, response.History) } } } func (handler *AWSHandler) CurrentCostHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("cost_usage_total") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ce.total", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeCostAndUsage(handler.cfg) + response, err := handler.aws.DescribeCostAndUsage(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ce:GetCostAndUsage is missing") } else { - handler.cache.Set("cost_usage_total", response.Total) + handler.cache.Set(key, response.Total) respondWithJSON(w, 200, response.Total) } } } func (handler *AWSHandler) CostAndUsagePerInstanceTypeHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_cost_per_instance_type") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ce.instance_type", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeCostAndUsagePerInstanceType(handler.cfg) + response, err := handler.aws.DescribeCostAndUsagePerInstanceType(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ce:GetCostAndUsage is missing") } else { - handler.cache.Set("aws_cost_per_instance_type", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) DescribeForecastPriceHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_cost_forecast") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ce.forecast", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeForecastPrice(handler.cfg) + response, err := handler.aws.DescribeForecastPrice(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ce:GetCostForecast is missing") } else { - handler.cache.Set("aws_cost_forecast", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/datapipeline_handler.go b/handlers/aws/datapipeline_handler.go index 8683312ac..7f4ebecb2 100644 --- a/handlers/aws/datapipeline_handler.go +++ b/handlers/aws/datapipeline_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) DataPipelineListPipelines(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_datapipeline_pipelines") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.datapipeline.pipelines", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.ListDataPipelines(handler.cfg) + response, err := handler.aws.ListDataPipelines(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "datapipeline:ListPipelines is missing") } else { - handler.cache.Set("aws_datapipeline_pipelines", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/dynamodb_handler.go b/handlers/aws/dynamodb_handler.go index b9091ffe0..cc4e9444f 100644 --- a/handlers/aws/dynamodb_handler.go +++ b/handlers/aws/dynamodb_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) DynamoDBTableHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_dynamodb") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.dynamodb.tables", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeDynamoDBTables(handler.cfg) + response, err := handler.aws.DescribeDynamoDBTables(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "dynamodb:ListTables or dynamodb:DescribeTable is missing") } else { - handler.cache.Set("aws_dynamodb", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/ebs_handler.go b/handlers/aws/ebs_handler.go index c04cc691e..9f4fe60b6 100644 --- a/handlers/aws/ebs_handler.go +++ b/handlers/aws/ebs_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) EBSHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_ebs") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ebs.disks", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeVolumes(handler.cfg) + response, err := handler.aws.DescribeVolumes(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeVolumes is missing") } else { - handler.cache.Set("aws_ebs", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/ec2_handler.go b/handlers/aws/ec2_handler.go index ba1878f97..8c1d56091 100644 --- a/handlers/aws/ec2_handler.go +++ b/handlers/aws/ec2_handler.go @@ -1,94 +1,169 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) EC2InstancesHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_ec2") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ec2.instances", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeInstances(handler.cfg) + response, err := handler.aws.DescribeInstances(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeInstances is missing") } else { - handler.cache.Set("aws_ec2", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) AutoScalingGroupHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_asg") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ec2.asg", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeAutoScalingGroups(handler.cfg) + response, err := handler.aws.DescribeAutoScalingGroups(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "autoscaling:DescribeAutoScalingGroups is missing") } else { - handler.cache.Set("aws_asg", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) ListUnrestrictedSecurityGroups(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_sg_unrestricted") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ec2.sg_unrestricted", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.ListUnrestrictedSecurityGroups(handler.cfg) + response, err := handler.aws.ListUnrestrictedSecurityGroups(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeSecurityGroups is missing") } else { - handler.cache.Set("aws_sg_unrestricted", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) ScheduledEC2Instances(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_ec2_scheduled") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ec2.scheduled", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeScheduledInstances(handler.cfg) + response, err := handler.aws.DescribeScheduledInstances(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeScheduledInstances is missing") } else { - handler.cache.Set("aws_ec2_scheduled", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) SpotEC2Instances(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_ec2_spot") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ec2.spot", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeSpotInstances(handler.cfg) + response, err := handler.aws.DescribeSpotInstances(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeSpotFleetRequests is missing") } else { - handler.cache.Set("aws_ec2_spot", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) ReservedEC2Instances(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_ec2_reserved") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.ec2.reserved", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeReservedInstances(handler.cfg) + response, err := handler.aws.DescribeReservedInstances(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeReservedInstances is missing") } else { - handler.cache.Set("aws_ec2_reserved", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/ecs_handler.go b/handlers/aws/ecs_handler.go index 74a2632c5..c088f9700 100644 --- a/handlers/aws/ecs_handler.go +++ b/handlers/aws/ecs_handler.go @@ -1,19 +1,30 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) ECSHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_ecs") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + + key := fmt.Sprintf("aws.%s.ecs.details", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeECS(handler.cfg) + response, err := handler.aws.DescribeECS(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ecs:DescribeClusters or ecs:DescribeTasks or ecs:DescribeServices is missing") } else { - handler.cache.Set("aws_ecs", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/eks_handler.go b/handlers/aws/eks_handler.go index 7a283a625..cec38cfdd 100644 --- a/handlers/aws/eks_handler.go +++ b/handlers/aws/eks_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) EKSClustersHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_eks_clusters") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.eks.clusters", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeEKSClusters(handler.cfg) + response, err := handler.aws.DescribeEKSClusters(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "eks:ListClusters is missing") } else { - handler.cache.Set("aws_eks_clusters", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/elasticache_handler.go b/handlers/aws/elasticache_handler.go index b795a10a8..bec1185a3 100644 --- a/handlers/aws/elasticache_handler.go +++ b/handlers/aws/elasticache_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) ElasticacheClustersHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_elasticache") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.elasticache.clusters", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeCacheClusters(handler.cfg) + response, err := handler.aws.DescribeCacheClusters(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "elasticache:DescribeCacheClusters is missing") } else { - handler.cache.Set("aws_elasticache", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/elb_handler.go b/handlers/aws/elb_handler.go index 4dcf86b95..431758bac 100644 --- a/handlers/aws/elb_handler.go +++ b/handlers/aws/elb_handler.go @@ -1,34 +1,61 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) ElasticLoadBalancerHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_elb") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.elb.total", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeElasticLoadBalancer(handler.cfg) + response, err := handler.aws.DescribeElasticLoadBalancer(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "elasticloadbalancing:DescribeLoadBalancers is missing") } else { - handler.cache.Set("aws_elb", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) ELBRequestsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_elb_requests") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.elb.requests", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetELBRequests(handler.cfg) + response, err := handler.aws.GetELBRequests(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "elasticloadbalancing:DescribeLoadBalancers is missing") } else { - handler.cache.Set("aws_elb_requests", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/es_handler.go b/handlers/aws/es_handler.go index d535ed495..056768d78 100644 --- a/handlers/aws/es_handler.go +++ b/handlers/aws/es_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) ESListDomainsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_es_domains") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.es.clusters", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.ListESDomains(handler.cfg) + response, err := handler.aws.ListESDomains(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "es:ListDomainNames is missing") } else { - handler.cache.Set("aws_es_domains", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/glue_handler.go b/handlers/aws/glue_handler.go index 3ea06375e..06f3cba2a 100644 --- a/handlers/aws/glue_handler.go +++ b/handlers/aws/glue_handler.go @@ -1,34 +1,61 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) GlueGetCrawlersHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_glue_crawlers") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.glue.crawlers", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetGlueCrawlers(handler.cfg) + response, err := handler.aws.GetGlueCrawlers(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "glue:GetCrawlers is missing") } else { - handler.cache.Set("aws_glue_crawlers", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) GlueGetJobsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_glue_jobs") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.glue.jobs", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetGlueJobs(handler.cfg) + response, err := handler.aws.GetGlueJobs(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "glue:GetJobs is missing") } else { - handler.cache.Set("aws_glue_jobs", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/handler.go b/handlers/aws/handler.go index 89114b7cc..3c43b1d50 100644 --- a/handlers/aws/handler.go +++ b/handlers/aws/handler.go @@ -4,22 +4,21 @@ import ( "encoding/json" "net/http" - "github.com/aws/aws-sdk-go-v2/aws" . "github.com/mlabouardy/komiser/services/aws" . "github.com/mlabouardy/komiser/services/cache" ) type AWSHandler struct { - cfg aws.Config - cache Cache - aws AWS + cache Cache + multiple bool + aws AWS } -func NewAWSHandler(cfg aws.Config, cache Cache) *AWSHandler { +func NewAWSHandler(cache Cache, multiple bool) *AWSHandler { awsHandler := AWSHandler{ - cfg: cfg, - cache: cache, - aws: AWS{}, + cache: cache, + multiple: multiple, + aws: AWS{}, } return &awsHandler } diff --git a/handlers/aws/iam_handler.go b/handlers/aws/iam_handler.go index 28be8c954..341f49e48 100644 --- a/handlers/aws/iam_handler.go +++ b/handlers/aws/iam_handler.go @@ -1,94 +1,169 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) IAMRolesHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_role") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.iam.roles", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeIAMRoles(handler.cfg) + response, err := handler.aws.DescribeIAMRoles(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "iam:ListRoles is missing") } else { - handler.cache.Set("aws_role", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) IAMGroupsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_group") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.iam.groups", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeIAMGroups(handler.cfg) + response, err := handler.aws.DescribeIAMGroups(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "iam:ListGroups is missing") } else { - handler.cache.Set("aws_group", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) IAMPoliciesHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_policy") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.iam.policies", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeIAMPolicies(handler.cfg) + response, err := handler.aws.DescribeIAMPolicies(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "iam:ListPolicies is missing") } else { - handler.cache.Set("aws_policy", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) IAMUsersHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_users") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.iam.users", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeIAMUsers(handler.cfg) + response, err := handler.aws.DescribeIAMUsers(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "iam:ListUsers is missing") } else { - handler.cache.Set("aws_users", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) IAMUserHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_user") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.iam.user", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeIAMUser(handler.cfg) + response, err := handler.aws.DescribeIAMUser(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "iam:GetUser is missing") } else { - handler.cache.Set("aws_user", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) DescribeOrganizationHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_organization") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.iam.organization", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeOrganization(handler.cfg) + response, err := handler.aws.DescribeOrganization(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "organizations:DescribeOrganization is missing") } else { - handler.cache.Set("aws_organization", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/kinesis_handler.go b/handlers/aws/kinesis_handler.go index 1f3f966ea..c38847b18 100644 --- a/handlers/aws/kinesis_handler.go +++ b/handlers/aws/kinesis_handler.go @@ -1,34 +1,61 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) KinesisListStreamsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_kinesis_streams") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.kinesis.streams", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.ListStreams(handler.cfg) + response, err := handler.aws.ListStreams(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "kinesis:ListStreams is missing") } else { - handler.cache.Set("aws_kinesis_streams", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) KinesisListShardsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_kinesis_shards") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.kinesis.shards", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.ListShards(handler.cfg) + response, err := handler.aws.ListShards(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "kinesis:ListShards is missing") } else { - handler.cache.Set("aws_kinesis_shards", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/kms_handler.go b/handlers/aws/kms_handler.go index bb58b1213..e8ed86f62 100644 --- a/handlers/aws/kms_handler.go +++ b/handlers/aws/kms_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) KMSKeysHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_kms_keys") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.kms.keys", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.ListKeys(handler.cfg) + response, err := handler.aws.ListKeys(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "kms:ListKeys is missing") } else { - handler.cache.Set("aws_kms_keys", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/lambda_handler.go b/handlers/aws/lambda_handler.go index d30c1bad9..d8847de3e 100644 --- a/handlers/aws/lambda_handler.go +++ b/handlers/aws/lambda_handler.go @@ -1,49 +1,88 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) LambdaFunctionHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_lambda_functions") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.lambda.functions", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeLambdaFunctions(handler.cfg) + response, err := handler.aws.DescribeLambdaFunctions(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "lambda:ListFunctions is missing") } else { - handler.cache.Set("aws_lambda_functions", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) GetLambdaInvocationMetrics(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_lambda_invocations") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.lambda.invocations", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetLambdaInvocationMetrics(handler.cfg) + response, err := handler.aws.GetLambdaInvocationMetrics(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:GetMetricStatistics is missing") } else { - handler.cache.Set("aws_lambda_invocations", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) GetLambdaErrorsMetrics(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_lambda_errors") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.lambda.errors", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetLambdaErrorsMetrics(handler.cfg) + response, err := handler.aws.GetLambdaErrorsMetrics(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:GetMetricStatistics is missing") } else { - handler.cache.Set("aws_lambda_errors", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/logs_handler.go b/handlers/aws/logs_handler.go index 5d40b0996..52e1d9572 100644 --- a/handlers/aws/logs_handler.go +++ b/handlers/aws/logs_handler.go @@ -1,34 +1,61 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) LogsVolumeHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_logs_volume") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.logs.volume", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetLogsVolume(handler.cfg) + response, err := handler.aws.GetLogsVolume(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:GetMetricStatistics is missing") } else { - handler.cache.Set("aws_logs_volume", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) MaximumLogsRetentionPeriodHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_logs_retention") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.logs.retention", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.MaximumLogsRetentionPeriod(handler.cfg) + response, err := handler.aws.MaximumLogsRetentionPeriod(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "logs:DescribeLogGroups is missing") } else { - handler.cache.Set("aws_logs_retention", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/rds_handler.go b/handlers/aws/rds_handler.go index b7acdbe50..8206d81cf 100644 --- a/handlers/aws/rds_handler.go +++ b/handlers/aws/rds_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) RDSInstanceHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_rds") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.rds.instances", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeRDSInstances(handler.cfg) + response, err := handler.aws.DescribeRDSInstances(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "rds:DescribeDBInstances is missing") } else { - handler.cache.Set("aws_rds", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/redshift_handler.go b/handlers/aws/redshift_handler.go index b050016e2..297f26766 100644 --- a/handlers/aws/redshift_handler.go +++ b/handlers/aws/redshift_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) DescribeRedshiftClustersHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_redshift_clusters") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.redshift.clusters", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeRedshiftClusters(handler.cfg) + response, err := handler.aws.DescribeRedshiftClusters(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "redshift:DescribeClusters is missing") } else { - handler.cache.Set("aws_redshift_clusters", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/resources_handler.go b/handlers/aws/resources_handler.go index 1989e54b9..5225adb59 100644 --- a/handlers/aws/resources_handler.go +++ b/handlers/aws/resources_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) UsedRegionsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_used_regions") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.resources.regions", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeResources(handler.cfg) + response, err := handler.aws.DescribeResources(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "tag:GetResources is missing") } else { - handler.cache.Set("aws_used_regions", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/route53_handler.go b/handlers/aws/route53_handler.go index f68a477fd..7df0d318f 100644 --- a/handlers/aws/route53_handler.go +++ b/handlers/aws/route53_handler.go @@ -1,34 +1,61 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) Route53HostedZonesHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_route53_zones") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.route53.zones", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeHostedZones(handler.cfg) + response, err := handler.aws.DescribeHostedZones(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "route53:ListHostedZones is missing") } else { - handler.cache.Set("aws_route53_zones", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) Route53ARecordsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_route53_a_records") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.route53.records", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeARecords(handler.cfg) + response, err := handler.aws.DescribeARecords(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "route53:ListResourceRecordSets is missing") } else { - handler.cache.Set("aws_route53_a_records", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/s3_handler.go b/handlers/aws/s3_handler.go index 8361cf7c7..f285596f2 100644 --- a/handlers/aws/s3_handler.go +++ b/handlers/aws/s3_handler.go @@ -1,64 +1,115 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) S3BucketsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_s3") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.s3.buckets", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeS3Buckets(handler.cfg) + response, err := handler.aws.DescribeS3Buckets(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "s3:ListAllMyBuckets is missing") } else { - handler.cache.Set("aws_s3", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) S3BucketsSizeHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_s3_size") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.s3.size", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetBucketsSize(handler.cfg) + response, err := handler.aws.GetBucketsSize(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:GetMetricStatistics is missing") } else { - handler.cache.Set("aws_s3_size", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) S3BucketsObjectsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_s3_objects") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.s3.objects", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetBucketsObjects(handler.cfg) + response, err := handler.aws.GetBucketsObjects(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:GetMetricStatistics is missing") } else { - handler.cache.Set("aws_s3_objects", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) GetEmptyBucketsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_s3_empty") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.s3.empty", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetEmptyBuckets(handler.cfg) + response, err := handler.aws.GetEmptyBuckets(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:GetMetricStatistics is missing") } else { - handler.cache.Set("aws_s3_empty", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/snapshot_handler.go b/handlers/aws/snapshot_handler.go index fecbbc34f..49d7f3635 100644 --- a/handlers/aws/snapshot_handler.go +++ b/handlers/aws/snapshot_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) SnapshotHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_snapshot") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.snapshots.total", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeSnapshots(handler.cfg) + response, err := handler.aws.DescribeSnapshots(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeSnapshots is missing") } else { - handler.cache.Set("aws_snapshot", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/sns_handler.go b/handlers/aws/sns_handler.go index 4076ef904..f718a0e87 100644 --- a/handlers/aws/sns_handler.go +++ b/handlers/aws/sns_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) SNSTopicsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_sns") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.sns.topics", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeSNSTopics(handler.cfg) + response, err := handler.aws.DescribeSNSTopics(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "sns:ListTopics is missing") } else { - handler.cache.Set("aws_sns", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/sqs_handler.go b/handlers/aws/sqs_handler.go index ff88bfe83..0816826de 100644 --- a/handlers/aws/sqs_handler.go +++ b/handlers/aws/sqs_handler.go @@ -1,34 +1,61 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) SQSQueuesHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_sqs") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.sqs.queues", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeQueues(handler.cfg) + response, err := handler.aws.DescribeQueues(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "sqs:ListQueues is missing") } else { - handler.cache.Set("aws_sqs", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) GetNumberOfMessagesSentAndDeletedSQSHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_sqs_messages") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.sqs.messages", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetNumberOfMessagesSentAndDeletedSQS(handler.cfg) + response, err := handler.aws.GetNumberOfMessagesSentAndDeletedSQS(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:GetMetricStatistics is missing") } else { - handler.cache.Set("aws_sqs_messages", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/support_handler.go b/handlers/aws/support_handler.go index 82e54d266..c26784520 100644 --- a/handlers/aws/support_handler.go +++ b/handlers/aws/support_handler.go @@ -1,49 +1,88 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) SupportOpenTicketsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_support_open_tickets") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.support.open", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.OpenSupportTickets(handler.cfg) + response, err := handler.aws.OpenSupportTickets(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "support:DescribeCases is missing") } else { - handler.cache.Set("aws_support_open_tickets", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) SupportTicketsInLastSixMonthsHandlers(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_support_history") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.support.history", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.TicketsInLastSixMonthsTickets(handler.cfg) + response, err := handler.aws.TicketsInLastSixMonthsTickets(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "support:DescribeCases is missing") } else { - handler.cache.Set("aws_support_history", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) DescribeServiceLimitsChecks(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_service_limits") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.support.limits", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeServiceLimitsChecks(handler.cfg) + response, err := handler.aws.DescribeServiceLimitsChecks(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "support:DescribeTrustedAdvisorChecks is missing") } else { - handler.cache.Set("aws_service_limits", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/swf_handler.go b/handlers/aws/swf_handler.go index 939f774bf..27053fe1b 100644 --- a/handlers/aws/swf_handler.go +++ b/handlers/aws/swf_handler.go @@ -1,19 +1,34 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) SWFListDomainsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_swf_domains") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.swf.domains", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetSWFDomains(handler.cfg) + response, err := handler.aws.GetSWFDomains(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "swf:ListDomains is missing") } else { - handler.cache.Set("aws_swf_domains", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/handlers/aws/vpc_handler.go b/handlers/aws/vpc_handler.go index 554c33e5c..15c6466a4 100644 --- a/handlers/aws/vpc_handler.go +++ b/handlers/aws/vpc_handler.go @@ -1,154 +1,275 @@ package aws import ( + "fmt" "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/external" ) func (handler *AWSHandler) VPCHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_vpc") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + key := fmt.Sprintf("aws.%s.vpc.total", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeVPCsTotal(handler.cfg) + response, err := handler.aws.DescribeVPCsTotal(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeVpcs is missing") } else { - handler.cache.Set("aws_vpc", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) ACLHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_acl") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.vpc.acl", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeACLsTotal(handler.cfg) + response, err := handler.aws.DescribeACLsTotal(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeNetworkAcls is missing") } else { - handler.cache.Set("aws_acl", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) SecurityGroupHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_sg") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.vpc.sg", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeSecurityGroupsTotal(handler.cfg) + response, err := handler.aws.DescribeSecurityGroupsTotal(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeSecurityGroups is missing") } else { - handler.cache.Set("aws_sg", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) NatGatewayHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_nat") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.vpc.nat", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeNatGatewaysTotal(handler.cfg) + response, err := handler.aws.DescribeNatGatewaysTotal(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeNatGateways is missing") } else { - handler.cache.Set("aws_nat", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) ElasticIPHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_eip") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.vpc.eip", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeElasticIPsTotal(handler.cfg) + response, err := handler.aws.DescribeElasticIPsTotal(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeElasticIPs is missing") } else { - handler.cache.Set("aws_eip", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) InternetGatewayHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_igw") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.vpc.igw", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeInternetGatewaysTotal(handler.cfg) + response, err := handler.aws.DescribeInternetGatewaysTotal(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeInternetGateways is missing") } else { - handler.cache.Set("aws_igw", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) RouteTableHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_rt") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.vpc.rt", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeRouteTablesTotal(handler.cfg) + response, err := handler.aws.DescribeRouteTablesTotal(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeRouteTables is missing") } else { - handler.cache.Set("aws_rt", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) KeyPairHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_kp") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.vpc.kp", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeKeyPairsTotal(handler.cfg) + response, err := handler.aws.DescribeKeyPairsTotal(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeKeyPairs is missing") } else { - handler.cache.Set("aws_kp", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) GetNatGatewayTrafficHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_nat_traffic") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + key := fmt.Sprintf("aws.%s.vpc.traffic", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.GetNatGatewayTraffic(handler.cfg) + response, err := handler.aws.GetNatGatewayTraffic(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "cloudwatch:GetMetricStatistics is missing") } else { - handler.cache.Set("aws_nat_traffic", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } } func (handler *AWSHandler) DescribeSubnetsHandler(w http.ResponseWriter, r *http.Request) { - response, found := handler.cache.Get("aws_subnets") + profile := r.Header.Get("profile") + cfg, err := external.LoadDefaultAWSConfig() + + if handler.multiple { + cfg, err = external.LoadDefaultAWSConfig(external.WithSharedConfigProfile(profile)) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't read "+profile+" profile") + } + } + + key := fmt.Sprintf("aws.%s.vpc.subnets", profile) + + response, found := handler.cache.Get(key) if found { respondWithJSON(w, 200, response) } else { - response, err := handler.aws.DescribeSubnets(handler.cfg) + response, err := handler.aws.DescribeSubnets(cfg) if err != nil { respondWithError(w, http.StatusInternalServerError, "ec2:DescribeSubnets is missing") } else { - handler.cache.Set("aws_subnets", response) + handler.cache.Set(key, response) respondWithJSON(w, 200, response) } } diff --git a/main.go b/main.go index 30dfbdbd9..b0f61156e 100644 --- a/main.go +++ b/main.go @@ -7,12 +7,12 @@ import ( "os" "time" - "github.com/aws/aws-sdk-go-v2/aws/external" "github.com/gorilla/handlers" "github.com/gorilla/mux" . "github.com/mlabouardy/komiser/handlers/aws" . "github.com/mlabouardy/komiser/handlers/gcp" . "github.com/mlabouardy/komiser/services/cache" + . "github.com/mlabouardy/komiser/services/ini" "github.com/urfave/cli" ) @@ -21,18 +21,14 @@ const ( DEFAULT_DURATION = 30 ) -func startServer(port int, cache Cache, dataset string) { - cfg, err := external.LoadDefaultAWSConfig() - if err != nil { - log.Fatal(err) - } - +func startServer(port int, cache Cache, dataset string, multiple bool) { cache.Connect() gcpHandler := NewGCPHandler(cache, dataset) - awsHandler := NewAWSHandler(cfg, cache) + awsHandler := NewAWSHandler(cache, multiple) r := mux.NewRouter() + r.HandleFunc("/aws/profiles", awsHandler.ConfigProfilesHandler) r.HandleFunc("/aws/iam/users", awsHandler.IAMUsersHandler) r.HandleFunc("/aws/iam/account", awsHandler.IAMUserHandler) r.HandleFunc("/aws/cost/current", awsHandler.CurrentCostHandler) @@ -145,10 +141,11 @@ func startServer(port int, cache Cache, dataset string) { r.HandleFunc("/gcp/iam/service_accounts", gcpHandler.IAMServiceAccountsHandler) r.HandleFunc("/gcp/dataflow/jobs", gcpHandler.DataflowJobsHandler) r.HandleFunc("/gcp/nat/gateways", gcpHandler.NatGatewaysHandler) - r.PathPrefix("/").Handler(http.FileServer(assetFS())) - loggedRouter := handlers.LoggingHandler(os.Stdout, handlers.CORS()(r)) - err = http.ListenAndServe(fmt.Sprintf(":%d", port), loggedRouter) + + headersOk := handlers.AllowedHeaders([]string{"profile"}) + loggedRouter := handlers.LoggingHandler(os.Stdout, handlers.CORS(headersOk)(r)) + err := http.ListenAndServe(fmt.Sprintf(":%d", port), loggedRouter) if err != nil { log.Fatal(err) } else { @@ -159,7 +156,7 @@ func startServer(port int, cache Cache, dataset string) { func main() { app := cli.NewApp() app.Name = "Komiser" - app.Version = "2.1.0" + app.Version = "2.2.0" app.Usage = "Cloud Environment Inspector" app.Copyright = "Komiser - https://komiser.io" app.Compiled = time.Now() @@ -192,12 +189,17 @@ func main() { Name: "dataset, ds", Usage: "BigQuery Bill dataset", }, + cli.BoolFlag{ + Name: "multiple, m", + Usage: "Enable multiple AWS accounts", + }, }, Action: func(c *cli.Context) error { port := c.Int("port") duration := c.Int("duration") redis := c.String("redis") dataset := c.String("dataset") + multiple := c.Bool("multiple") var cache Cache @@ -219,7 +221,7 @@ func main() { } } - startServer(port, cache, dataset) + startServer(port, cache, dataset, multiple) return nil }, }, diff --git a/policy.json b/policy.json index 1eb34ec25..2cdfe59e0 100644 --- a/policy.json +++ b/policy.json @@ -44,9 +44,9 @@ "rds:DescribeDBInstances", "cloudwatch:DescribeAlarms", "cloudfront:ListDistributions", - "ecs:DescribeClusters", - "ecs:DescribeServices", - "ecs:DescribeTasks" + "ecs:ListServices", + "ecs:ListTasks", + "ecs:ListClusters" ], "Resource": "*" }, diff --git a/services/aws/ecs.go b/services/aws/ecs.go index 7ba82146a..37c94d187 100644 --- a/services/aws/ecs.go +++ b/services/aws/ecs.go @@ -23,12 +23,12 @@ func (aws AWS) DescribeECS(cfg aws.Config) (map[string]int, error) { } countClusters += len(clusters) for _, cluster := range clusters { - tasks, err := aws.getTasks(cfg, cluster.Name, region.Name) + tasks, err := aws.getTasks(cfg, cluster, region.Name) if err != nil { return map[string]int{}, err } countTasks += len(tasks) - services, err := aws.getServices(cfg, cluster.Name, region.Name) + services, err := aws.getServices(cfg, cluster, region.Name) if err != nil { return map[string]int{}, err } @@ -42,57 +42,30 @@ func (aws AWS) DescribeECS(cfg aws.Config) (map[string]int, error) { }, nil } -func (aws AWS) getClusters(cfg aws.Config, region string) ([]Cluster, error) { +func (aws AWS) getClusters(cfg aws.Config, region string) ([]string, error) { cfg.Region = region svc := ecs.New(cfg) - req := svc.DescribeClustersRequest(&ecs.DescribeClustersInput{}) + req := svc.ListClustersRequest(&ecs.ListClustersInput{}) result, err := req.Send(context.Background()) - if err != nil { - return []Cluster{}, err - } - listOfClusters := make([]Cluster, 0, len(result.Clusters)) - for _, cluster := range result.Clusters { - listOfClusters = append(listOfClusters, Cluster{ - Name: *cluster.ClusterName, - }) - } - return listOfClusters, nil + return result.ClusterArns, err } -func (aws AWS) getTasks(cfg aws.Config, cluster string, region string) ([]Task, error) { +func (aws AWS) getTasks(cfg aws.Config, cluster string, region string) ([]string, error) { cfg.Region = region svc := ecs.New(cfg) - req := svc.DescribeTasksRequest(&ecs.DescribeTasksInput{ + req := svc.ListTasksRequest(&ecs.ListTasksInput{ Cluster: &cluster, }) result, err := req.Send(context.Background()) - if err != nil { - return []Task{}, err - } - listOfTasks := make([]Task, 0, len(result.Tasks)) - for _, task := range result.Tasks { - listOfTasks = append(listOfTasks, Task{ - ARN: *task.TaskArn, - }) - } - return listOfTasks, nil + return result.TaskArns, err } -func (aws AWS) getServices(cfg aws.Config, cluster string, region string) ([]Service, error) { +func (aws AWS) getServices(cfg aws.Config, cluster string, region string) ([]string, error) { cfg.Region = region svc := ecs.New(cfg) - req := svc.DescribeServicesRequest(&ecs.DescribeServicesInput{ + req := svc.ListServicesRequest(&ecs.ListServicesInput{ Cluster: &cluster, }) result, err := req.Send(context.Background()) - if err != nil { - return []Service{}, err - } - listOfServices := make([]Service, 0, len(result.Services)) - for _, service := range result.Services { - listOfServices = append(listOfServices, Service{ - Name: *service.ServiceName, - }) - } - return listOfServices, nil + return result.ServiceArns, err } diff --git a/services/ini/ast.go b/services/ini/ast.go new file mode 100644 index 000000000..e83a99886 --- /dev/null +++ b/services/ini/ast.go @@ -0,0 +1,120 @@ +package ini + +// ASTKind represents different states in the parse table +// and the type of AST that is being constructed +type ASTKind int + +// ASTKind* is used in the parse table to transition between +// the different states +const ( + ASTKindNone = ASTKind(iota) + ASTKindStart + ASTKindExpr + ASTKindEqualExpr + ASTKindStatement + ASTKindSkipStatement + ASTKindExprStatement + ASTKindSectionStatement + ASTKindNestedSectionStatement + ASTKindCompletedNestedSectionStatement + ASTKindCommentStatement + ASTKindCompletedSectionStatement +) + +func (k ASTKind) String() string { + switch k { + case ASTKindNone: + return "none" + case ASTKindStart: + return "start" + case ASTKindExpr: + return "expr" + case ASTKindStatement: + return "stmt" + case ASTKindSectionStatement: + return "section_stmt" + case ASTKindExprStatement: + return "expr_stmt" + case ASTKindCommentStatement: + return "comment" + case ASTKindNestedSectionStatement: + return "nested_section_stmt" + case ASTKindCompletedSectionStatement: + return "completed_stmt" + case ASTKindSkipStatement: + return "skip" + default: + return "" + } +} + +// AST interface allows us to determine what kind of node we +// are on and casting may not need to be necessary. +// +// The root is always the first node in Children +type AST struct { + Kind ASTKind + Root Token + RootToken bool + Children []AST +} + +func newAST(kind ASTKind, root AST, children ...AST) AST { + return AST{ + Kind: kind, + Children: append([]AST{root}, children...), + } +} + +func newASTWithRootToken(kind ASTKind, root Token, children ...AST) AST { + return AST{ + Kind: kind, + Root: root, + RootToken: true, + Children: children, + } +} + +// AppendChild will append to the list of children an AST has. +func (a *AST) AppendChild(child AST) { + a.Children = append(a.Children, child) +} + +// GetRoot will return the root AST which can be the first entry +// in the children list or a token. +func (a *AST) GetRoot() AST { + if a.RootToken { + return *a + } + + if len(a.Children) == 0 { + return AST{} + } + + return a.Children[0] +} + +// GetChildren will return the current AST's list of children +func (a *AST) GetChildren() []AST { + if len(a.Children) == 0 { + return []AST{} + } + + if a.RootToken { + return a.Children + } + + return a.Children[1:] +} + +// SetChildren will set and override all children of the AST. +func (a *AST) SetChildren(children []AST) { + if a.RootToken { + a.Children = children + } else { + a.Children = append(a.Children[:1], children...) + } +} + +// Start is used to indicate the starting state of the parse table. +var Start = newAST(ASTKindStart, AST{}) diff --git a/services/ini/comma_token.go b/services/ini/comma_token.go new file mode 100644 index 000000000..0895d53cb --- /dev/null +++ b/services/ini/comma_token.go @@ -0,0 +1,11 @@ +package ini + +var commaRunes = []rune(",") + +func isComma(b rune) bool { + return b == ',' +} + +func newCommaToken() Token { + return newToken(TokenComma, commaRunes, NoneType) +} diff --git a/services/ini/comment_token.go b/services/ini/comment_token.go new file mode 100644 index 000000000..0b76999ba --- /dev/null +++ b/services/ini/comment_token.go @@ -0,0 +1,35 @@ +package ini + +// isComment will return whether or not the next byte(s) is a +// comment. +func isComment(b []rune) bool { + if len(b) == 0 { + return false + } + + switch b[0] { + case ';': + return true + case '#': + return true + } + + return false +} + +// newCommentToken will create a comment token and +// return how many bytes were read. +func newCommentToken(b []rune) (Token, int, error) { + i := 0 + for ; i < len(b); i++ { + if b[i] == '\n' { + break + } + + if len(b)-i > 2 && b[i] == '\r' && b[i+1] == '\n' { + break + } + } + + return newToken(TokenComment, b[:i], NoneType), i, nil +} diff --git a/services/ini/doc.go b/services/ini/doc.go new file mode 100644 index 000000000..25ce0fe13 --- /dev/null +++ b/services/ini/doc.go @@ -0,0 +1,29 @@ +// Package ini is an LL(1) parser for configuration files. +// +// Example: +// sections, err := ini.OpenFile("/path/to/file") +// if err != nil { +// panic(err) +// } +// +// profile := "foo" +// section, ok := sections.GetSection(profile) +// if !ok { +// fmt.Printf("section %q could not be found", profile) +// } +// +// Below is the BNF that describes this parser +// Grammar: +// stmt -> value stmt' +// stmt' -> epsilon | op stmt +// value -> number | string | boolean | quoted_string +// +// section -> [ section' +// section' -> value section_close +// section_close -> ] +// +// SkipState will skip (NL WS)+ +// +// comment -> # comment' | ; comment' +// comment' -> epsilon | value +package ini diff --git a/services/ini/empty_token.go b/services/ini/empty_token.go new file mode 100644 index 000000000..04345a54c --- /dev/null +++ b/services/ini/empty_token.go @@ -0,0 +1,4 @@ +package ini + +// emptyToken is used to satisfy the Token interface +var emptyToken = newToken(TokenNone, []rune{}, NoneType) diff --git a/services/ini/expression.go b/services/ini/expression.go new file mode 100644 index 000000000..91ba2a59d --- /dev/null +++ b/services/ini/expression.go @@ -0,0 +1,24 @@ +package ini + +// newExpression will return an expression AST. +// Expr represents an expression +// +// grammar: +// expr -> string | number +func newExpression(tok Token) AST { + return newASTWithRootToken(ASTKindExpr, tok) +} + +func newEqualExpr(left AST, tok Token) AST { + return newASTWithRootToken(ASTKindEqualExpr, tok, left) +} + +// EqualExprKey will return a LHS value in the equal expr +func EqualExprKey(ast AST) string { + children := ast.GetChildren() + if len(children) == 0 || ast.Kind != ASTKindEqualExpr { + return "" + } + + return string(children[0].Root.Raw()) +} diff --git a/services/ini/fuzz.go b/services/ini/fuzz.go new file mode 100644 index 000000000..8d462f77e --- /dev/null +++ b/services/ini/fuzz.go @@ -0,0 +1,17 @@ +// +build gofuzz + +package ini + +import ( + "bytes" +) + +func Fuzz(data []byte) int { + b := bytes.NewReader(data) + + if _, err := Parse(b); err != nil { + return 0 + } + + return 1 +} diff --git a/services/ini/ini.go b/services/ini/ini.go new file mode 100644 index 000000000..af6f397d2 --- /dev/null +++ b/services/ini/ini.go @@ -0,0 +1,51 @@ +package ini + +import ( + "io" + "os" + + "github.com/aws/aws-sdk-go-v2/aws/awserr" +) + +// OpenFile takes a path to a given file, and will open and parse +// that file. +func OpenFile(path string) (Sections, error) { + f, err := os.Open(path) + if err != nil { + return Sections{}, awserr.New(ErrCodeUnableToReadFile, "unable to open file", err) + } + defer f.Close() + + return Parse(f) +} + +// Parse will parse the given file using the shared config +// visitor. +func Parse(f io.Reader) (Sections, error) { + tree, err := ParseAST(f) + if err != nil { + return Sections{}, err + } + + v := NewDefaultVisitor() + if err = Walk(tree, v); err != nil { + return Sections{}, err + } + + return v.Sections, nil +} + +// ParseBytes will parse the given bytes and return the parsed sections. +func ParseBytes(b []byte) (Sections, error) { + tree, err := ParseASTBytes(b) + if err != nil { + return Sections{}, err + } + + v := NewDefaultVisitor() + if err = Walk(tree, v); err != nil { + return Sections{}, err + } + + return v.Sections, nil +} diff --git a/services/ini/ini_lexer.go b/services/ini/ini_lexer.go new file mode 100644 index 000000000..898ebb05c --- /dev/null +++ b/services/ini/ini_lexer.go @@ -0,0 +1,165 @@ +package ini + +import ( + "bytes" + "io" + "io/ioutil" + + "github.com/aws/aws-sdk-go-v2/aws/awserr" +) + +const ( + // ErrCodeUnableToReadFile is used when a file is failed to be + // opened or read from. + ErrCodeUnableToReadFile = "FailedRead" +) + +// TokenType represents the various different tokens types +type TokenType int + +func (t TokenType) String() string { + switch t { + case TokenNone: + return "none" + case TokenLit: + return "literal" + case TokenSep: + return "sep" + case TokenOp: + return "op" + case TokenWS: + return "ws" + case TokenNL: + return "newline" + case TokenComment: + return "comment" + case TokenComma: + return "comma" + default: + return "" + } +} + +// TokenType enums +const ( + TokenNone = TokenType(iota) + TokenLit + TokenSep + TokenComma + TokenOp + TokenWS + TokenNL + TokenComment +) + +type iniLexer struct{} + +// Tokenize will return a list of tokens during lexical analysis of the +// io.Reader. +func (l *iniLexer) Tokenize(r io.Reader) ([]Token, error) { + b, err := ioutil.ReadAll(r) + if err != nil { + return nil, awserr.New(ErrCodeUnableToReadFile, "unable to read file", err) + } + + return l.tokenize(b) +} + +func (l *iniLexer) tokenize(b []byte) ([]Token, error) { + runes := bytes.Runes(b) + var err error + n := 0 + tokenAmount := countTokens(runes) + tokens := make([]Token, tokenAmount) + count := 0 + + for len(runes) > 0 && count < tokenAmount { + switch { + case isWhitespace(runes[0]): + tokens[count], n, err = newWSToken(runes) + case isComma(runes[0]): + tokens[count], n = newCommaToken(), 1 + case isComment(runes): + tokens[count], n, err = newCommentToken(runes) + case isNewline(runes): + tokens[count], n, err = newNewlineToken(runes) + case isSep(runes): + tokens[count], n, err = newSepToken(runes) + case isOp(runes): + tokens[count], n, err = newOpToken(runes) + default: + tokens[count], n, err = newLitToken(runes) + } + + if err != nil { + return nil, err + } + + count++ + + runes = runes[n:] + } + + return tokens[:count], nil +} + +func countTokens(runes []rune) int { + count, n := 0, 0 + var err error + + for len(runes) > 0 { + switch { + case isWhitespace(runes[0]): + _, n, err = newWSToken(runes) + case isComma(runes[0]): + _, n = newCommaToken(), 1 + case isComment(runes): + _, n, err = newCommentToken(runes) + case isNewline(runes): + _, n, err = newNewlineToken(runes) + case isSep(runes): + _, n, err = newSepToken(runes) + case isOp(runes): + _, n, err = newOpToken(runes) + default: + _, n, err = newLitToken(runes) + } + + if err != nil { + return 0 + } + + count++ + runes = runes[n:] + } + + return count + 1 +} + +// Token indicates a metadata about a given value. +type Token struct { + t TokenType + ValueType ValueType + base int + raw []rune +} + +var emptyValue = Value{} + +func newToken(t TokenType, raw []rune, v ValueType) Token { + return Token{ + t: t, + raw: raw, + ValueType: v, + } +} + +// Raw return the raw runes that were consumed +func (tok Token) Raw() []rune { + return tok.raw +} + +// Type returns the token type +func (tok Token) Type() TokenType { + return tok.t +} diff --git a/services/ini/ini_parser.go b/services/ini/ini_parser.go new file mode 100644 index 000000000..8be520ae6 --- /dev/null +++ b/services/ini/ini_parser.go @@ -0,0 +1,347 @@ +package ini + +import ( + "fmt" + "io" +) + +// State enums for the parse table +const ( + InvalidState = iota + // stmt -> value stmt' + StatementState + // stmt' -> MarkComplete | op stmt + StatementPrimeState + // value -> number | string | boolean | quoted_string + ValueState + // section -> [ section' + OpenScopeState + // section' -> value section_close + SectionState + // section_close -> ] + CloseScopeState + // SkipState will skip (NL WS)+ + SkipState + // SkipTokenState will skip any token and push the previous + // state onto the stack. + SkipTokenState + // comment -> # comment' | ; comment' + // comment' -> MarkComplete | value + CommentState + // MarkComplete state will complete statements and move that + // to the completed AST list + MarkCompleteState + // TerminalState signifies that the tokens have been fully parsed + TerminalState +) + +// parseTable is a state machine to dictate the grammar above. +var parseTable = map[ASTKind]map[TokenType]int{ + ASTKindStart: map[TokenType]int{ + TokenLit: StatementState, + TokenSep: OpenScopeState, + TokenWS: SkipTokenState, + TokenNL: SkipTokenState, + TokenComment: CommentState, + TokenNone: TerminalState, + }, + ASTKindCommentStatement: map[TokenType]int{ + TokenLit: StatementState, + TokenSep: OpenScopeState, + TokenWS: SkipTokenState, + TokenNL: SkipTokenState, + TokenComment: CommentState, + TokenNone: MarkCompleteState, + }, + ASTKindExpr: map[TokenType]int{ + TokenOp: StatementPrimeState, + TokenLit: ValueState, + TokenSep: OpenScopeState, + TokenWS: ValueState, + TokenNL: SkipState, + TokenComment: CommentState, + TokenNone: MarkCompleteState, + }, + ASTKindEqualExpr: map[TokenType]int{ + TokenLit: ValueState, + TokenWS: SkipTokenState, + TokenNL: SkipState, + }, + ASTKindStatement: map[TokenType]int{ + TokenLit: SectionState, + TokenSep: CloseScopeState, + TokenWS: SkipTokenState, + TokenNL: SkipTokenState, + TokenComment: CommentState, + TokenNone: MarkCompleteState, + }, + ASTKindExprStatement: map[TokenType]int{ + TokenLit: ValueState, + TokenSep: OpenScopeState, + TokenOp: ValueState, + TokenWS: ValueState, + TokenNL: MarkCompleteState, + TokenComment: CommentState, + TokenNone: TerminalState, + TokenComma: SkipState, + }, + ASTKindSectionStatement: map[TokenType]int{ + TokenLit: SectionState, + TokenOp: SectionState, + TokenSep: CloseScopeState, + TokenWS: SectionState, + TokenNL: SkipTokenState, + }, + ASTKindCompletedSectionStatement: map[TokenType]int{ + TokenWS: SkipTokenState, + TokenNL: SkipTokenState, + TokenLit: StatementState, + TokenSep: OpenScopeState, + TokenComment: CommentState, + TokenNone: MarkCompleteState, + }, + ASTKindSkipStatement: map[TokenType]int{ + TokenLit: StatementState, + TokenSep: OpenScopeState, + TokenWS: SkipTokenState, + TokenNL: SkipTokenState, + TokenComment: CommentState, + TokenNone: TerminalState, + }, +} + +// ParseAST will parse input from an io.Reader using +// an LL(1) parser. +func ParseAST(r io.Reader) ([]AST, error) { + lexer := iniLexer{} + tokens, err := lexer.Tokenize(r) + if err != nil { + return []AST{}, err + } + + return parse(tokens) +} + +// ParseASTBytes will parse input from a byte slice using +// an LL(1) parser. +func ParseASTBytes(b []byte) ([]AST, error) { + lexer := iniLexer{} + tokens, err := lexer.tokenize(b) + if err != nil { + return []AST{}, err + } + + return parse(tokens) +} + +func parse(tokens []Token) ([]AST, error) { + start := Start + stack := newParseStack(3, len(tokens)) + + stack.Push(start) + s := newSkipper() + +loop: + for stack.Len() > 0 { + k := stack.Pop() + + var tok Token + if len(tokens) == 0 { + // this occurs when all the tokens have been processed + // but reduction of what's left on the stack needs to + // occur. + tok = emptyToken + } else { + tok = tokens[0] + } + + step := parseTable[k.Kind][tok.Type()] + if s.ShouldSkip(tok) { + // being in a skip state with no tokens will break out of + // the parse loop since there is nothing left to process. + if len(tokens) == 0 { + break loop + } + + step = SkipTokenState + } + + switch step { + case TerminalState: + // Finished parsing. Push what should be the last + // statement to the stack. If there is anything left + // on the stack, an error in parsing has occurred. + if k.Kind != ASTKindStart { + stack.MarkComplete(k) + } + break loop + case SkipTokenState: + // When skipping a token, the previous state was popped off the stack. + // To maintain the correct state, the previous state will be pushed + // onto the stack. + stack.Push(k) + case StatementState: + if k.Kind != ASTKindStart { + stack.MarkComplete(k) + } + expr := newExpression(tok) + stack.Push(expr) + case StatementPrimeState: + if tok.Type() != TokenOp { + stack.MarkComplete(k) + continue + } + + if k.Kind != ASTKindExpr { + return nil, NewParseError( + fmt.Sprintf("invalid expression: expected Expr type, but found %T type", k), + ) + } + + k = trimSpaces(k) + expr := newEqualExpr(k, tok) + stack.Push(expr) + case ValueState: + // ValueState requires the previous state to either be an equal expression + // or an expression statement. + // + // This grammar occurs when the RHS is a number, word, or quoted string. + // equal_expr -> lit op equal_expr' + // equal_expr' -> number | string | quoted_string + // quoted_string -> " quoted_string' + // quoted_string' -> string quoted_string_end + // quoted_string_end -> " + // + // otherwise + // expr_stmt -> equal_expr (expr_stmt')* + // expr_stmt' -> ws S | op S | MarkComplete + // S -> equal_expr' expr_stmt' + switch k.Kind { + case ASTKindEqualExpr: + // assiging a value to some key + k.AppendChild(newExpression(tok)) + stack.Push(newExprStatement(k)) + case ASTKindExpr: + k.Root.raw = append(k.Root.raw, tok.Raw()...) + stack.Push(k) + case ASTKindExprStatement: + root := k.GetRoot() + children := root.GetChildren() + if len(children) == 0 { + return nil, NewParseError( + fmt.Sprintf("invalid expression: AST contains no children %s", k.Kind), + ) + } + + rhs := children[len(children)-1] + + if rhs.Root.ValueType != QuotedStringType { + rhs.Root.ValueType = StringType + rhs.Root.raw = append(rhs.Root.raw, tok.Raw()...) + + } + + children[len(children)-1] = rhs + k.SetChildren(children) + + stack.Push(k) + } + case OpenScopeState: + if !runeCompare(tok.Raw(), openBrace) { + return nil, NewParseError("expected '['") + } + + stmt := newStatement() + stack.Push(stmt) + case CloseScopeState: + if !runeCompare(tok.Raw(), closeBrace) { + return nil, NewParseError("expected ']'") + } + + k = trimSpaces(k) + stack.Push(newCompletedSectionStatement(k)) + case SectionState: + var stmt AST + + switch k.Kind { + case ASTKindStatement: + // If there are multiple literals inside of a scope declaration, + // then the current token's raw value will be appended to the Name. + // + // This handles cases like [ profile default ] + // + // k will represent a SectionStatement with the children representing + // the label of the section + stmt = newSectionStatement(tok) + case ASTKindSectionStatement: + k.Root.raw = append(k.Root.raw, tok.Raw()...) + stmt = k + default: + return nil, NewParseError( + fmt.Sprintf("invalid statement: expected statement: %v", k.Kind), + ) + } + + stack.Push(stmt) + case MarkCompleteState: + if k.Kind != ASTKindStart { + stack.MarkComplete(k) + } + + if stack.Len() == 0 { + stack.Push(start) + } + case SkipState: + stack.Push(newSkipStatement(k)) + s.Skip() + case CommentState: + if k.Kind == ASTKindStart { + stack.Push(k) + } else { + stack.MarkComplete(k) + } + + stmt := newCommentStatement(tok) + stack.Push(stmt) + default: + return nil, NewParseError(fmt.Sprintf("invalid state with ASTKind %v and TokenType %v", k, tok)) + } + + if len(tokens) > 0 { + tokens = tokens[1:] + } + } + + // this occurs when a statement has not been completed + if stack.top > 1 { + return nil, NewParseError(fmt.Sprintf("incomplete expression: %v", stack.container)) + } + + // returns a sublist which exludes the start symbol + return stack.List(), nil +} + +// trimSpaces will trim spaces on the left and right hand side of +// the literal. +func trimSpaces(k AST) AST { + // trim left hand side of spaces + for i := 0; i < len(k.Root.raw); i++ { + if !isWhitespace(k.Root.raw[i]) { + break + } + + k.Root.raw = k.Root.raw[1:] + i-- + } + + // trim right hand side of spaces + for i := len(k.Root.raw) - 1; i >= 0; i-- { + if !isWhitespace(k.Root.raw[i]) { + break + } + + k.Root.raw = k.Root.raw[:len(k.Root.raw)-1] + } + + return k +} diff --git a/services/ini/literal_tokens.go b/services/ini/literal_tokens.go new file mode 100644 index 000000000..24df543d3 --- /dev/null +++ b/services/ini/literal_tokens.go @@ -0,0 +1,324 @@ +package ini + +import ( + "fmt" + "strconv" + "strings" +) + +var ( + runesTrue = []rune("true") + runesFalse = []rune("false") +) + +var literalValues = [][]rune{ + runesTrue, + runesFalse, +} + +func isBoolValue(b []rune) bool { + for _, lv := range literalValues { + if isLitValue(lv, b) { + return true + } + } + return false +} + +func isLitValue(want, have []rune) bool { + if len(have) < len(want) { + return false + } + + for i := 0; i < len(want); i++ { + if want[i] != have[i] { + return false + } + } + + return true +} + +// isNumberValue will return whether not the leading characters in +// a byte slice is a number. A number is delimited by whitespace or +// the newline token. +// +// A number is defined to be in a binary, octal, decimal (int | float), hex format, +// or in scientific notation. +func isNumberValue(b []rune) bool { + negativeIndex := 0 + helper := numberHelper{} + needDigit := false + + for i := 0; i < len(b); i++ { + negativeIndex++ + + switch b[i] { + case '-': + if helper.IsNegative() || negativeIndex != 1 { + return false + } + helper.Determine(b[i]) + needDigit = true + continue + case 'e', 'E': + if err := helper.Determine(b[i]); err != nil { + return false + } + negativeIndex = 0 + needDigit = true + continue + case 'b': + if helper.numberFormat == hex { + break + } + fallthrough + case 'o', 'x': + needDigit = true + if i == 0 { + return false + } + + fallthrough + case '.': + if err := helper.Determine(b[i]); err != nil { + return false + } + needDigit = true + continue + } + + if i > 0 && (isNewline(b[i:]) || isWhitespace(b[i])) { + return !needDigit + } + + if !helper.CorrectByte(b[i]) { + return false + } + needDigit = false + } + + return !needDigit +} + +func isValid(b []rune) (bool, int, error) { + if len(b) == 0 { + // TODO: should probably return an error + return false, 0, nil + } + + return isValidRune(b[0]), 1, nil +} + +func isValidRune(r rune) bool { + return r != ':' && r != '=' && r != '[' && r != ']' && r != ' ' && r != '\n' +} + +// ValueType is an enum that will signify what type +// the Value is +type ValueType int + +func (v ValueType) String() string { + switch v { + case NoneType: + return "NONE" + case DecimalType: + return "FLOAT" + case IntegerType: + return "INT" + case StringType: + return "STRING" + case BoolType: + return "BOOL" + } + + return "" +} + +// ValueType enums +const ( + NoneType = ValueType(iota) + DecimalType + IntegerType + StringType + QuotedStringType + BoolType +) + +// Value is a union container +type Value struct { + Type ValueType + raw []rune + + integer int64 + decimal float64 + boolean bool + str string +} + +func newValue(t ValueType, base int, raw []rune) (Value, error) { + v := Value{ + Type: t, + raw: raw, + } + var err error + + switch t { + case DecimalType: + v.decimal, err = strconv.ParseFloat(string(raw), 64) + case IntegerType: + if base != 10 { + raw = raw[2:] + } + + v.integer, err = strconv.ParseInt(string(raw), base, 64) + case StringType: + v.str = string(raw) + case QuotedStringType: + v.str = string(raw[1 : len(raw)-1]) + case BoolType: + v.boolean = runeCompare(v.raw, runesTrue) + } + + // issue 2253 + // + // if the value trying to be parsed is too large, then we will use + // the 'StringType' and raw value instead. + if nerr, ok := err.(*strconv.NumError); ok && nerr.Err == strconv.ErrRange { + v.Type = StringType + v.str = string(raw) + err = nil + } + + return v, err +} + +// Append will append values and change the type to a string +// type. +func (v *Value) Append(tok Token) { + r := tok.Raw() + if v.Type != QuotedStringType { + v.Type = StringType + r = tok.raw[1 : len(tok.raw)-1] + } + if tok.Type() != TokenLit { + v.raw = append(v.raw, tok.Raw()...) + } else { + v.raw = append(v.raw, r...) + } +} + +func (v Value) String() string { + switch v.Type { + case DecimalType: + return fmt.Sprintf("decimal: %f", v.decimal) + case IntegerType: + return fmt.Sprintf("integer: %d", v.integer) + case StringType: + return fmt.Sprintf("string: %s", string(v.raw)) + case QuotedStringType: + return fmt.Sprintf("quoted string: %s", string(v.raw)) + case BoolType: + return fmt.Sprintf("bool: %t", v.boolean) + default: + return "union not set" + } +} + +func newLitToken(b []rune) (Token, int, error) { + n := 0 + var err error + + token := Token{} + if b[0] == '"' { + n, err = getStringValue(b) + if err != nil { + return token, n, err + } + + token = newToken(TokenLit, b[:n], QuotedStringType) + } else if isNumberValue(b) { + var base int + base, n, err = getNumericalValue(b) + if err != nil { + return token, 0, err + } + + value := b[:n] + vType := IntegerType + if contains(value, '.') || hasExponent(value) { + vType = DecimalType + } + token = newToken(TokenLit, value, vType) + token.base = base + } else if isBoolValue(b) { + n, err = getBoolValue(b) + + token = newToken(TokenLit, b[:n], BoolType) + } else { + n, err = getValue(b) + token = newToken(TokenLit, b[:n], StringType) + } + + return token, n, err +} + +// IntValue returns an integer value +func (v Value) IntValue() int64 { + return v.integer +} + +// FloatValue returns a float value +func (v Value) FloatValue() float64 { + return v.decimal +} + +// BoolValue returns a bool value +func (v Value) BoolValue() bool { + return v.boolean +} + +func isTrimmable(r rune) bool { + switch r { + case '\n', ' ': + return true + } + return false +} + +// StringValue returns the string value +func (v Value) StringValue() string { + switch v.Type { + case StringType: + return strings.TrimFunc(string(v.raw), isTrimmable) + case QuotedStringType: + // preserve all characters in the quotes + return string(removeEscapedCharacters(v.raw[1 : len(v.raw)-1])) + default: + return strings.TrimFunc(string(v.raw), isTrimmable) + } +} + +func contains(runes []rune, c rune) bool { + for i := 0; i < len(runes); i++ { + if runes[i] == c { + return true + } + } + + return false +} + +func runeCompare(v1 []rune, v2 []rune) bool { + if len(v1) != len(v2) { + return false + } + + for i := 0; i < len(v1); i++ { + if v1[i] != v2[i] { + return false + } + } + + return true +} diff --git a/services/ini/newline_token.go b/services/ini/newline_token.go new file mode 100644 index 000000000..e52ac399f --- /dev/null +++ b/services/ini/newline_token.go @@ -0,0 +1,30 @@ +package ini + +func isNewline(b []rune) bool { + if len(b) == 0 { + return false + } + + if b[0] == '\n' { + return true + } + + if len(b) < 2 { + return false + } + + return b[0] == '\r' && b[1] == '\n' +} + +func newNewlineToken(b []rune) (Token, int, error) { + i := 1 + if b[0] == '\r' && isNewline(b[1:]) { + i++ + } + + if !isNewline([]rune(b[:i])) { + return emptyToken, 0, NewParseError("invalid new line token") + } + + return newToken(TokenNL, b[:i], NoneType), i, nil +} diff --git a/services/ini/number_helper.go b/services/ini/number_helper.go new file mode 100644 index 000000000..a45c0bc56 --- /dev/null +++ b/services/ini/number_helper.go @@ -0,0 +1,152 @@ +package ini + +import ( + "bytes" + "fmt" + "strconv" +) + +const ( + none = numberFormat(iota) + binary + octal + decimal + hex + exponent +) + +type numberFormat int + +// numberHelper is used to dictate what format a number is in +// and what to do for negative values. Since -1e-4 is a valid +// number, we cannot just simply check for duplicate negatives. +type numberHelper struct { + numberFormat numberFormat + + negative bool + negativeExponent bool +} + +func (b numberHelper) Exists() bool { + return b.numberFormat != none +} + +func (b numberHelper) IsNegative() bool { + return b.negative || b.negativeExponent +} + +func (b *numberHelper) Determine(c rune) error { + if b.Exists() { + return NewParseError(fmt.Sprintf("multiple number formats: 0%v", string(c))) + } + + switch c { + case 'b': + b.numberFormat = binary + case 'o': + b.numberFormat = octal + case 'x': + b.numberFormat = hex + case 'e', 'E': + b.numberFormat = exponent + case '-': + if b.numberFormat != exponent { + b.negative = true + } else { + b.negativeExponent = true + } + case '.': + b.numberFormat = decimal + default: + return NewParseError(fmt.Sprintf("invalid number character: %v", string(c))) + } + + return nil +} + +func (b numberHelper) CorrectByte(c rune) bool { + switch { + case b.numberFormat == binary: + if !isBinaryByte(c) { + return false + } + case b.numberFormat == octal: + if !isOctalByte(c) { + return false + } + case b.numberFormat == hex: + if !isHexByte(c) { + return false + } + case b.numberFormat == decimal: + if !isDigit(c) { + return false + } + case b.numberFormat == exponent: + if !isDigit(c) { + return false + } + case b.negativeExponent: + if !isDigit(c) { + return false + } + case b.negative: + if !isDigit(c) { + return false + } + default: + if !isDigit(c) { + return false + } + } + + return true +} + +func (b numberHelper) Base() int { + switch b.numberFormat { + case binary: + return 2 + case octal: + return 8 + case hex: + return 16 + default: + return 10 + } +} + +func (b numberHelper) String() string { + buf := bytes.Buffer{} + i := 0 + + switch b.numberFormat { + case binary: + i++ + buf.WriteString(strconv.Itoa(i) + ": binary format\n") + case octal: + i++ + buf.WriteString(strconv.Itoa(i) + ": octal format\n") + case hex: + i++ + buf.WriteString(strconv.Itoa(i) + ": hex format\n") + case exponent: + i++ + buf.WriteString(strconv.Itoa(i) + ": exponent format\n") + default: + i++ + buf.WriteString(strconv.Itoa(i) + ": integer format\n") + } + + if b.negative { + i++ + buf.WriteString(strconv.Itoa(i) + ": negative format\n") + } + + if b.negativeExponent { + i++ + buf.WriteString(strconv.Itoa(i) + ": negative exponent format\n") + } + + return buf.String() +} diff --git a/services/ini/op_tokens.go b/services/ini/op_tokens.go new file mode 100644 index 000000000..8a84c7cbe --- /dev/null +++ b/services/ini/op_tokens.go @@ -0,0 +1,39 @@ +package ini + +import ( + "fmt" +) + +var ( + equalOp = []rune("=") + equalColonOp = []rune(":") +) + +func isOp(b []rune) bool { + if len(b) == 0 { + return false + } + + switch b[0] { + case '=': + return true + case ':': + return true + default: + return false + } +} + +func newOpToken(b []rune) (Token, int, error) { + tok := Token{} + + switch b[0] { + case '=': + tok = newToken(TokenOp, equalOp, NoneType) + case ':': + tok = newToken(TokenOp, equalColonOp, NoneType) + default: + return tok, 0, NewParseError(fmt.Sprintf("unexpected op type, %v", b[0])) + } + return tok, 1, nil +} diff --git a/services/ini/parse_error.go b/services/ini/parse_error.go new file mode 100644 index 000000000..457287019 --- /dev/null +++ b/services/ini/parse_error.go @@ -0,0 +1,43 @@ +package ini + +import "fmt" + +const ( + // ErrCodeParseError is returned when a parsing error + // has occurred. + ErrCodeParseError = "INIParseError" +) + +// ParseError is an error which is returned during any part of +// the parsing process. +type ParseError struct { + msg string +} + +// NewParseError will return a new ParseError where message +// is the description of the error. +func NewParseError(message string) *ParseError { + return &ParseError{ + msg: message, + } +} + +// Code will return the ErrCodeParseError +func (err *ParseError) Code() string { + return ErrCodeParseError +} + +// Message returns the error's message +func (err *ParseError) Message() string { + return err.msg +} + +// OrigError return nothing since there will never be any +// original error. +func (err *ParseError) OrigError() error { + return nil +} + +func (err *ParseError) Error() string { + return fmt.Sprintf("%s: %s", err.Code(), err.Message()) +} diff --git a/services/ini/parse_stack.go b/services/ini/parse_stack.go new file mode 100644 index 000000000..7f01cf7c7 --- /dev/null +++ b/services/ini/parse_stack.go @@ -0,0 +1,60 @@ +package ini + +import ( + "bytes" + "fmt" +) + +// ParseStack is a stack that contains a container, the stack portion, +// and the list which is the list of ASTs that have been successfully +// parsed. +type ParseStack struct { + top int + container []AST + list []AST + index int +} + +func newParseStack(sizeContainer, sizeList int) ParseStack { + return ParseStack{ + container: make([]AST, sizeContainer), + list: make([]AST, sizeList), + } +} + +// Pop will return and truncate the last container element. +func (s *ParseStack) Pop() AST { + s.top-- + return s.container[s.top] +} + +// Push will add the new AST to the container +func (s *ParseStack) Push(ast AST) { + s.container[s.top] = ast + s.top++ +} + +// MarkComplete will append the AST to the list of completed statements +func (s *ParseStack) MarkComplete(ast AST) { + s.list[s.index] = ast + s.index++ +} + +// List will return the completed statements +func (s ParseStack) List() []AST { + return s.list[:s.index] +} + +// Len will return the length of the container +func (s *ParseStack) Len() int { + return s.top +} + +func (s ParseStack) String() string { + buf := bytes.Buffer{} + for i, node := range s.list { + buf.WriteString(fmt.Sprintf("%d: %v\n", i+1, node)) + } + + return buf.String() +} diff --git a/services/ini/sep_tokens.go b/services/ini/sep_tokens.go new file mode 100644 index 000000000..f82095ba2 --- /dev/null +++ b/services/ini/sep_tokens.go @@ -0,0 +1,41 @@ +package ini + +import ( + "fmt" +) + +var ( + emptyRunes = []rune{} +) + +func isSep(b []rune) bool { + if len(b) == 0 { + return false + } + + switch b[0] { + case '[', ']': + return true + default: + return false + } +} + +var ( + openBrace = []rune("[") + closeBrace = []rune("]") +) + +func newSepToken(b []rune) (Token, int, error) { + tok := Token{} + + switch b[0] { + case '[': + tok = newToken(TokenSep, openBrace, NoneType) + case ']': + tok = newToken(TokenSep, closeBrace, NoneType) + default: + return tok, 0, NewParseError(fmt.Sprintf("unexpected sep type, %v", b[0])) + } + return tok, 1, nil +} diff --git a/services/ini/skipper.go b/services/ini/skipper.go new file mode 100644 index 000000000..6bb696447 --- /dev/null +++ b/services/ini/skipper.go @@ -0,0 +1,45 @@ +package ini + +// skipper is used to skip certain blocks of an ini file. +// Currently skipper is used to skip nested blocks of ini +// files. See example below +// +// [ foo ] +// nested = ; this section will be skipped +// a=b +// c=d +// bar=baz ; this will be included +type skipper struct { + shouldSkip bool + TokenSet bool + prevTok Token +} + +func newSkipper() skipper { + return skipper{ + prevTok: emptyToken, + } +} + +func (s *skipper) ShouldSkip(tok Token) bool { + if s.shouldSkip && + s.prevTok.Type() == TokenNL && + tok.Type() != TokenWS { + + s.Continue() + return false + } + s.prevTok = tok + + return s.shouldSkip +} + +func (s *skipper) Skip() { + s.shouldSkip = true + s.prevTok = emptyToken +} + +func (s *skipper) Continue() { + s.shouldSkip = false + s.prevTok = emptyToken +} diff --git a/services/ini/statement.go b/services/ini/statement.go new file mode 100644 index 000000000..ba0af01b5 --- /dev/null +++ b/services/ini/statement.go @@ -0,0 +1,35 @@ +package ini + +// Statement is an empty AST mostly used for transitioning states. +func newStatement() AST { + return newAST(ASTKindStatement, AST{}) +} + +// SectionStatement represents a section AST +func newSectionStatement(tok Token) AST { + return newASTWithRootToken(ASTKindSectionStatement, tok) +} + +// ExprStatement represents a completed expression AST +func newExprStatement(ast AST) AST { + return newAST(ASTKindExprStatement, ast) +} + +// CommentStatement represents a comment in the ini defintion. +// +// grammar: +// comment -> #comment' | ;comment' +// comment' -> epsilon | value +func newCommentStatement(tok Token) AST { + return newAST(ASTKindCommentStatement, newExpression(tok)) +} + +// CompletedSectionStatement represents a completed section +func newCompletedSectionStatement(ast AST) AST { + return newAST(ASTKindCompletedSectionStatement, ast) +} + +// SkipStatement is used to skip whole statements +func newSkipStatement(ast AST) AST { + return newAST(ASTKindSkipStatement, ast) +} diff --git a/services/ini/value_util.go b/services/ini/value_util.go new file mode 100644 index 000000000..305999d29 --- /dev/null +++ b/services/ini/value_util.go @@ -0,0 +1,284 @@ +package ini + +import ( + "fmt" +) + +// getStringValue will return a quoted string and the amount +// of bytes read +// +// an error will be returned if the string is not properly formatted +func getStringValue(b []rune) (int, error) { + if b[0] != '"' { + return 0, NewParseError("strings must start with '\"'") + } + + endQuote := false + i := 1 + + for ; i < len(b) && !endQuote; i++ { + if escaped := isEscaped(b[:i], b[i]); b[i] == '"' && !escaped { + endQuote = true + break + } else if escaped { + /*c, err := getEscapedByte(b[i]) + if err != nil { + return 0, err + } + + b[i-1] = c + b = append(b[:i], b[i+1:]...) + i--*/ + + continue + } + } + + if !endQuote { + return 0, NewParseError("missing '\"' in string value") + } + + return i + 1, nil +} + +// getBoolValue will return a boolean and the amount +// of bytes read +// +// an error will be returned if the boolean is not of a correct +// value +func getBoolValue(b []rune) (int, error) { + if len(b) < 4 { + return 0, NewParseError("invalid boolean value") + } + + n := 0 + for _, lv := range literalValues { + if len(lv) > len(b) { + continue + } + + if isLitValue(lv, b) { + n = len(lv) + } + } + + if n == 0 { + return 0, NewParseError("invalid boolean value") + } + + return n, nil +} + +// getNumericalValue will return a numerical string, the amount +// of bytes read, and the base of the number +// +// an error will be returned if the number is not of a correct +// value +func getNumericalValue(b []rune) (int, int, error) { + if !isDigit(b[0]) { + return 0, 0, NewParseError("invalid digit value") + } + + i := 0 + helper := numberHelper{} + +loop: + for negativeIndex := 0; i < len(b); i++ { + negativeIndex++ + + if !isDigit(b[i]) { + switch b[i] { + case '-': + if helper.IsNegative() || negativeIndex != 1 { + return 0, 0, NewParseError("parse error '-'") + } + + n := getNegativeNumber(b[i:]) + i += (n - 1) + helper.Determine(b[i]) + continue + case '.': + if err := helper.Determine(b[i]); err != nil { + return 0, 0, err + } + case 'e', 'E': + if err := helper.Determine(b[i]); err != nil { + return 0, 0, err + } + + negativeIndex = 0 + case 'b': + if helper.numberFormat == hex { + break + } + fallthrough + case 'o', 'x': + if i == 0 && b[i] != '0' { + return 0, 0, NewParseError("incorrect base format, expected leading '0'") + } + + if i != 1 { + return 0, 0, NewParseError(fmt.Sprintf("incorrect base format found %s at %d index", string(b[i]), i)) + } + + if err := helper.Determine(b[i]); err != nil { + return 0, 0, err + } + default: + if isWhitespace(b[i]) { + break loop + } + + if isNewline(b[i:]) { + break loop + } + + if !(helper.numberFormat == hex && isHexByte(b[i])) { + if i+2 < len(b) && !isNewline(b[i:i+2]) { + return 0, 0, NewParseError("invalid numerical character") + } else if !isNewline([]rune{b[i]}) { + return 0, 0, NewParseError("invalid numerical character") + } + + break loop + } + } + } + } + + return helper.Base(), i, nil +} + +// isDigit will return whether or not something is an integer +func isDigit(b rune) bool { + return b >= '0' && b <= '9' +} + +func hasExponent(v []rune) bool { + return contains(v, 'e') || contains(v, 'E') +} + +func isBinaryByte(b rune) bool { + switch b { + case '0', '1': + return true + default: + return false + } +} + +func isOctalByte(b rune) bool { + switch b { + case '0', '1', '2', '3', '4', '5', '6', '7': + return true + default: + return false + } +} + +func isHexByte(b rune) bool { + if isDigit(b) { + return true + } + return (b >= 'A' && b <= 'F') || + (b >= 'a' && b <= 'f') +} + +func getValue(b []rune) (int, error) { + i := 0 + + for i < len(b) { + if isNewline(b[i:]) { + break + } + + if isOp(b[i:]) { + break + } + + valid, n, err := isValid(b[i:]) + if err != nil { + return 0, err + } + + if !valid { + break + } + + i += n + } + + return i, nil +} + +// getNegativeNumber will return a negative number from a +// byte slice. This will iterate through all characters until +// a non-digit has been found. +func getNegativeNumber(b []rune) int { + if b[0] != '-' { + return 0 + } + + i := 1 + for ; i < len(b); i++ { + if !isDigit(b[i]) { + return i + } + } + + return i +} + +// isEscaped will return whether or not the character is an escaped +// character. +func isEscaped(value []rune, b rune) bool { + if len(value) == 0 { + return false + } + + switch b { + case '\'': // single quote + case '"': // quote + case 'n': // newline + case 't': // tab + case '\\': // backslash + default: + return false + } + + return value[len(value)-1] == '\\' +} + +func getEscapedByte(b rune) (rune, error) { + switch b { + case '\'': // single quote + return '\'', nil + case '"': // quote + return '"', nil + case 'n': // newline + return '\n', nil + case 't': // table + return '\t', nil + case '\\': // backslash + return '\\', nil + default: + return b, NewParseError(fmt.Sprintf("invalid escaped character %c", b)) + } +} + +func removeEscapedCharacters(b []rune) []rune { + for i := 0; i < len(b); i++ { + if isEscaped(b[:i], b[i]) { + c, err := getEscapedByte(b[i]) + if err != nil { + return b + } + + b[i-1] = c + b = append(b[:i], b[i+1:]...) + i-- + } + } + + return b +} diff --git a/services/ini/visitor.go b/services/ini/visitor.go new file mode 100644 index 000000000..94841c324 --- /dev/null +++ b/services/ini/visitor.go @@ -0,0 +1,166 @@ +package ini + +import ( + "fmt" + "sort" +) + +// Visitor is an interface used by walkers that will +// traverse an array of ASTs. +type Visitor interface { + VisitExpr(AST) error + VisitStatement(AST) error +} + +// DefaultVisitor is used to visit statements and expressions +// and ensure that they are both of the correct format. +// In addition, upon visiting this will build sections and populate +// the Sections field which can be used to retrieve profile +// configuration. +type DefaultVisitor struct { + scope string + Sections Sections +} + +// NewDefaultVisitor return a DefaultVisitor +func NewDefaultVisitor() *DefaultVisitor { + return &DefaultVisitor{ + Sections: Sections{ + container: map[string]Section{}, + }, + } +} + +// VisitExpr visits expressions... +func (v *DefaultVisitor) VisitExpr(expr AST) error { + t := v.Sections.container[v.scope] + if t.values == nil { + t.values = values{} + } + + switch expr.Kind { + case ASTKindExprStatement: + opExpr := expr.GetRoot() + switch opExpr.Kind { + case ASTKindEqualExpr: + children := opExpr.GetChildren() + if len(children) <= 1 { + return NewParseError("unexpected token type") + } + + rhs := children[1] + + if rhs.Root.Type() != TokenLit { + return NewParseError("unexpected token type") + } + + key := EqualExprKey(opExpr) + v, err := newValue(rhs.Root.ValueType, rhs.Root.base, rhs.Root.Raw()) + if err != nil { + return err + } + + t.values[key] = v + default: + return NewParseError(fmt.Sprintf("unsupported expression %v", expr)) + } + default: + return NewParseError(fmt.Sprintf("unsupported expression %v", expr)) + } + + v.Sections.container[v.scope] = t + return nil +} + +// VisitStatement visits statements... +func (v *DefaultVisitor) VisitStatement(stmt AST) error { + switch stmt.Kind { + case ASTKindCompletedSectionStatement: + child := stmt.GetRoot() + if child.Kind != ASTKindSectionStatement { + return NewParseError(fmt.Sprintf("unsupported child statement: %T", child)) + } + + name := string(child.Root.Raw()) + v.Sections.container[name] = Section{} + v.scope = name + default: + return NewParseError(fmt.Sprintf("unsupported statement: %s", stmt.Kind)) + } + + return nil +} + +// Sections is a map of Section structures that represent +// a configuration. +type Sections struct { + container map[string]Section +} + +// GetSection will return section p. If section p does not exist, +// false will be returned in the second parameter. +func (t Sections) GetSection(p string) (Section, bool) { + v, ok := t.container[p] + return v, ok +} + +// values represents a map of union values. +type values map[string]Value + +// List will return a list of all sections that were successfully +// parsed. +func (t Sections) List() []string { + keys := make([]string, len(t.container)) + i := 0 + for k := range t.container { + keys[i] = k + i++ + } + + sort.Strings(keys) + return keys +} + +// Section contains a name and values. This represent +// a sectioned entry in a configuration file. +type Section struct { + Name string + values values +} + +// Has will return whether or not an entry exists in a given section +func (t Section) Has(k string) bool { + _, ok := t.values[k] + return ok +} + +// ValueType will returned what type the union is set to. If +// k was not found, the NoneType will be returned. +func (t Section) ValueType(k string) (ValueType, bool) { + v, ok := t.values[k] + return v.Type, ok +} + +// Bool returns a bool value at k +func (t Section) Bool(k string) bool { + return t.values[k].BoolValue() +} + +// Int returns an integer value at k +func (t Section) Int(k string) int64 { + return t.values[k].IntValue() +} + +// Float64 returns a float value at k +func (t Section) Float64(k string) float64 { + return t.values[k].FloatValue() +} + +// String returns the string value at k +func (t Section) String(k string) string { + _, ok := t.values[k] + if !ok { + return "" + } + return t.values[k].StringValue() +} diff --git a/services/ini/walker.go b/services/ini/walker.go new file mode 100644 index 000000000..99915f7f7 --- /dev/null +++ b/services/ini/walker.go @@ -0,0 +1,25 @@ +package ini + +// Walk will traverse the AST using the v, the Visitor. +func Walk(tree []AST, v Visitor) error { + for _, node := range tree { + switch node.Kind { + case ASTKindExpr, + ASTKindExprStatement: + + if err := v.VisitExpr(node); err != nil { + return err + } + case ASTKindStatement, + ASTKindCompletedSectionStatement, + ASTKindNestedSectionStatement, + ASTKindCompletedNestedSectionStatement: + + if err := v.VisitStatement(node); err != nil { + return err + } + } + } + + return nil +} diff --git a/services/ini/ws_token.go b/services/ini/ws_token.go new file mode 100644 index 000000000..7ffb4ae06 --- /dev/null +++ b/services/ini/ws_token.go @@ -0,0 +1,24 @@ +package ini + +import ( + "unicode" +) + +// isWhitespace will return whether or not the character is +// a whitespace character. +// +// Whitespace is defined as a space or tab. +func isWhitespace(c rune) bool { + return unicode.IsSpace(c) && c != '\n' && c != '\r' +} + +func newWSToken(b []rune) (Token, int, error) { + i := 0 + for ; i < len(b); i++ { + if !isWhitespace(b[i]) { + break + } + } + + return newToken(TokenWS, b[:i], NoneType), i, nil +}