mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 08:09:24 +00:00
Feat: Add cloudformation stack-set support (#11268)
This commit is contained in:
@@ -126,6 +126,10 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
content(newVal: string) {
|
||||
this.displayData = newVal;
|
||||
this.refreshData();
|
||||
},
|
||||
computedData() {
|
||||
this.refreshData();
|
||||
},
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { mount, VueWrapper } from '@vue/test-utils';
|
||||
import { Quasar } from 'quasar';
|
||||
import { Quasar, Notify } from 'quasar';
|
||||
import { createStore } from 'vuex';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import AWSQuickSetup from './AWSQuickSetup.vue';
|
||||
|
||||
const { MOCK_SERVICES, MOCK_REGIONS } = vi.hoisted(() => ({
|
||||
MOCK_SERVICES: [
|
||||
{ label: 'CloudTrail', flag: 'EnableCloudTrail' },
|
||||
{ label: 'CloudWatch Metrics', flag: 'EnableCloudWatchMetrics' },
|
||||
{ label: 'VPC Flow Logs', flag: 'EnableVPCFlowLogs' },
|
||||
],
|
||||
MOCK_REGIONS: [
|
||||
{ value: 'us-east-1', label: 'US East (N. Virginia)' },
|
||||
{ value: 'us-west-2', label: 'US West (Oregon)' },
|
||||
{ value: 'eu-west-1', label: 'Europe (Ireland)' },
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock('../../../aws-exports', () => ({
|
||||
default: { API_ENDPOINT: 'http://localhost:5080', region: 'us-east-1' },
|
||||
}));
|
||||
@@ -22,36 +35,10 @@ vi.mock('@/utils/zincutils', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/awsIntegrations', () => ({
|
||||
awsIntegrations: [
|
||||
{
|
||||
id: 'cloudwatch-logs',
|
||||
name: 'CloudWatchLogs',
|
||||
displayName: 'CloudWatch Logs',
|
||||
description: 'Collect CloudWatch logs',
|
||||
category: 'logs',
|
||||
cloudFormationTemplate: 'https://example.com/cloudwatch.yaml',
|
||||
hasDashboard: true,
|
||||
},
|
||||
{
|
||||
id: 's3-events',
|
||||
name: 'S3Events',
|
||||
displayName: 'S3 Events',
|
||||
description: 'Collect S3 events',
|
||||
category: 'logs',
|
||||
cloudFormationTemplate: 'https://example.com/s3.yaml',
|
||||
hasDashboard: false,
|
||||
},
|
||||
{
|
||||
id: 'vpc-flow',
|
||||
name: 'VPCFlow',
|
||||
displayName: 'VPC Flow Logs',
|
||||
description: 'Collect VPC flow logs',
|
||||
category: 'networking',
|
||||
hasDashboard: false,
|
||||
},
|
||||
],
|
||||
awsIntegrations: [],
|
||||
generateCloudFormationURL: vi.fn(() => 'https://console.aws.amazon.com/cloudformation'),
|
||||
awsIntegrationCategories: { logs: 'Logs', networking: 'Networking' },
|
||||
AWS_REGIONS: MOCK_REGIONS,
|
||||
QUICK_SETUP_SERVICES: MOCK_SERVICES,
|
||||
}));
|
||||
|
||||
vi.mock('@/services/segment_analytics', () => ({
|
||||
@@ -77,7 +64,6 @@ describe('AWSQuickSetup.vue', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Mock window.open
|
||||
vi.stubGlobal('open', vi.fn());
|
||||
});
|
||||
|
||||
@@ -86,14 +72,13 @@ describe('AWSQuickSetup.vue', () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const createWrapper = () => {
|
||||
return mount(AWSQuickSetup, {
|
||||
const createWrapper = () =>
|
||||
mount(AWSQuickSetup, {
|
||||
global: {
|
||||
plugins: [Quasar, mockI18n, mockRouter],
|
||||
plugins: [[Quasar, { plugins: { Notify } }], mockI18n, mockRouter],
|
||||
provide: { store: mockStore },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('Component Rendering', () => {
|
||||
it('should mount without errors', () => {
|
||||
@@ -106,137 +91,115 @@ describe('AWSQuickSetup.vue', () => {
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it('should render Deploy Complete Stack button', () => {
|
||||
it('should have correct component name', () => {
|
||||
wrapper = createWrapper();
|
||||
const deployBtn = wrapper.find('[data-test="aws-quick-setup-deploy-btn"]');
|
||||
expect(deployBtn.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render View/Hide Details button', () => {
|
||||
wrapper = createWrapper();
|
||||
const detailsBtn = wrapper.find('[data-test="aws-quick-setup-details-btn"]');
|
||||
expect(detailsBtn.exists()).toBe(true);
|
||||
expect(wrapper.vm.$options.name).toBe('AWSQuickSetup');
|
||||
});
|
||||
|
||||
it('should render Complete AWS Integration title', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(wrapper.text()).toContain('Complete AWS Integration');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component Identity', () => {
|
||||
it('should have correct component name', () => {
|
||||
it('should render deployment mode toggle', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(wrapper.vm.$options.name).toBe('AWSQuickSetup');
|
||||
expect(wrapper.find('[data-test="aws-deployment-mode-toggle"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render launch button', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(wrapper.find('[data-test="aws-quick-setup-deploy-btn"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showDetails Toggle', () => {
|
||||
it('should have showDetails false by default', () => {
|
||||
describe('Deployment Mode', () => {
|
||||
it('should default to single region mode', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.showDetails).toBe(false);
|
||||
expect((wrapper.vm as any).deploymentMode).toBe('single');
|
||||
});
|
||||
|
||||
it('should toggle showDetails when details button is clicked', async () => {
|
||||
it('should show region select in single mode', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.showDetails).toBe(false);
|
||||
const detailsBtn = wrapper.find('[data-test="aws-quick-setup-details-btn"]');
|
||||
await detailsBtn.trigger('click');
|
||||
expect(vm.showDetails).toBe(true);
|
||||
expect(wrapper.find('[data-test="aws-region-select"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle showDetails back when clicked again', async () => {
|
||||
it('should default selected region to us-east-1', () => {
|
||||
wrapper = createWrapper();
|
||||
expect((wrapper.vm as any).selectedRegion).toBe('us-east-1');
|
||||
});
|
||||
|
||||
it('should switch to stackset mode', async () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
const detailsBtn = wrapper.find('[data-test="aws-quick-setup-details-btn"]');
|
||||
await detailsBtn.trigger('click');
|
||||
expect(vm.showDetails).toBe(true);
|
||||
await detailsBtn.trigger('click');
|
||||
expect(vm.showDetails).toBe(false);
|
||||
vm.deploymentMode = 'stackset';
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(vm.deploymentMode).toBe('stackset');
|
||||
});
|
||||
});
|
||||
|
||||
describe('includedServices Computed', () => {
|
||||
it('should compute includedServices with services having CloudFormation templates', () => {
|
||||
describe('Services Selection', () => {
|
||||
it('should start with all services enabled', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.includedServices).toBeDefined();
|
||||
expect(Array.isArray(vm.includedServices)).toBe(true);
|
||||
expect(vm.enabledServices.length).toBe(MOCK_SERVICES.length);
|
||||
});
|
||||
|
||||
it('should include services with cloudFormationTemplate', () => {
|
||||
it('should deselect all services', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
// CloudWatch Logs and S3 Events have cloudFormationTemplate
|
||||
expect(vm.includedServices.length).toBeGreaterThan(0);
|
||||
vm.deselectAll();
|
||||
expect(vm.enabledServices.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should sort services by name', () => {
|
||||
it('should select all services', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
const names = vm.includedServices.map((s: any) => s.name);
|
||||
const sortedNames = [...names].sort();
|
||||
expect(names).toEqual(sortedNames);
|
||||
vm.deselectAll();
|
||||
vm.selectAll();
|
||||
expect(vm.enabledServices.length).toBe(MOCK_SERVICES.length);
|
||||
});
|
||||
|
||||
it('should include service with name and description', () => {
|
||||
it('should start with services section collapsed', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
if (vm.includedServices.length > 0) {
|
||||
expect(vm.includedServices[0].name).toBeDefined();
|
||||
expect(vm.includedServices[0].description).toBeDefined();
|
||||
expect(vm.includedServices[0].category).toBeDefined();
|
||||
}
|
||||
expect((wrapper.vm as any).showServices).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCategoryIcon Function', () => {
|
||||
it('should return icon for logs category', () => {
|
||||
describe('StackSets Mode', () => {
|
||||
it('should default stackset model to self-managed', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.getCategoryIcon('logs')).toBe('description');
|
||||
expect((wrapper.vm as any).stackSetModel).toBe('self');
|
||||
});
|
||||
|
||||
it('should return icon for metrics category', () => {
|
||||
it('should default target regions to us-east-1', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.getCategoryIcon('metrics')).toBe('speed');
|
||||
expect((wrapper.vm as any).targetRegions).toEqual(['us-east-1']);
|
||||
});
|
||||
|
||||
it('should return icon for security category', () => {
|
||||
it('should start with target regions collapsed', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.getCategoryIcon('security')).toBe('security');
|
||||
expect((wrapper.vm as any).showTargetRegions).toBe(false);
|
||||
});
|
||||
|
||||
it('should return cloud icon for unknown category', () => {
|
||||
it('should select all regions', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.getCategoryIcon('unknown')).toBe('cloud');
|
||||
vm.selectAllRegions();
|
||||
expect(vm.targetRegions.length).toBe(MOCK_REGIONS.length);
|
||||
});
|
||||
|
||||
it('should clear target regions', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
vm.targetRegions = [];
|
||||
expect(vm.targetRegions.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatCategory Function', () => {
|
||||
it('should capitalize first letter', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.formatCategory('logs')).toBe('Logs');
|
||||
});
|
||||
|
||||
it('should capitalize security', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.formatCategory('security')).toBe('Security');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDeployStack', () => {
|
||||
describe('handleLaunch - Single Region', () => {
|
||||
it('should be exposed as a function', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
expect(typeof vm.handleDeployStack).toBe('function');
|
||||
expect(typeof (wrapper.vm as any).handleLaunch).toBe('function');
|
||||
});
|
||||
|
||||
it('should call window.open when endpoint is valid', async () => {
|
||||
@@ -245,5 +208,81 @@ describe('AWSQuickSetup.vue', () => {
|
||||
await deployBtn.trigger('click');
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call window.open when no services selected', async () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
vm.deselectAll();
|
||||
await wrapper.vm.$nextTick();
|
||||
const deployBtn = wrapper.find('[data-test="aws-quick-setup-deploy-btn"]');
|
||||
await deployBtn.trigger('click');
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleLaunch - StackSets', () => {
|
||||
it('should open stacksets console URL in stackset mode', async () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
vm.deploymentMode = 'stackset';
|
||||
await wrapper.vm.$nextTick();
|
||||
vm.handleLaunch();
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
expect.stringContaining('stacksets/create'),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show param helper after stackset launch', async () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
vm.deploymentMode = 'stackset';
|
||||
await wrapper.vm.$nextTick();
|
||||
vm.handleLaunch();
|
||||
expect(vm.showParamHelper).toBe(true);
|
||||
});
|
||||
|
||||
it('should not launch stackset when no target regions selected', async () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
vm.deploymentMode = 'stackset';
|
||||
vm.targetRegions = [];
|
||||
await wrapper.vm.$nextTick();
|
||||
vm.handleLaunch();
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stackSetParams computed', () => {
|
||||
it('should include template URL as first param', () => {
|
||||
wrapper = createWrapper();
|
||||
const vm = wrapper.vm as any;
|
||||
const params = vm.stackSetParams;
|
||||
expect(params[0].key).toBe('Amazon S3 URL (template)');
|
||||
expect(params[0].value).toContain('aws_complete.yaml');
|
||||
});
|
||||
|
||||
it('should include TemplateS3Bucket param', () => {
|
||||
wrapper = createWrapper();
|
||||
const params = (wrapper.vm as any).stackSetParams;
|
||||
const bucket = params.find((p: any) => p.key === 'TemplateS3Bucket');
|
||||
expect(bucket?.value).toBe('openobserve-datasources-bucket');
|
||||
});
|
||||
|
||||
it('should include OpenObserveEndpoint param', () => {
|
||||
wrapper = createWrapper();
|
||||
const params = (wrapper.vm as any).stackSetParams;
|
||||
const ep = params.find((p: any) => p.key === 'OpenObserveEndpoint');
|
||||
expect(ep?.value).toContain('localhost');
|
||||
});
|
||||
|
||||
it('should include a param for each service flag', () => {
|
||||
wrapper = createWrapper();
|
||||
const params = (wrapper.vm as any).stackSetParams;
|
||||
MOCK_SERVICES.forEach(({ flag }) => {
|
||||
expect(params.find((p: any) => p.key === flag)).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,117 +17,256 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<template>
|
||||
<div class="aws-quick-setup">
|
||||
<div class="setup-card">
|
||||
<div class="tw:pb-4">
|
||||
<div class="tw:flex tw:items-start tw:gap-4">
|
||||
<div class="tw:flex-shrink-0">
|
||||
<q-icon name="rocket_launch" size="2.5rem" color="primary" />
|
||||
</div>
|
||||
<div class="tw:flex-1">
|
||||
<h5 class="tw:text-lg tw:font-bold tw:m-0 tw:mb-2 title">
|
||||
Complete AWS Integration
|
||||
</h5>
|
||||
<p class="tw:text-sm tw:m-0 tw:mb-4 description">
|
||||
Deploy all AWS services in one click. This CloudFormation stack sets up comprehensive monitoring across your entire AWS infrastructure.
|
||||
</p>
|
||||
|
||||
<div class="tw:flex tw:gap-2 tw:mt-4">
|
||||
<q-btn
|
||||
color="primary"
|
||||
unelevated
|
||||
@click="handleDeployStack"
|
||||
data-test="aws-quick-setup-deploy-btn"
|
||||
>
|
||||
<q-icon name="cloud_upload" left size="sm" />
|
||||
Deploy Complete Stack
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
@click="showDetails = !showDetails"
|
||||
data-test="aws-quick-setup-details-btn"
|
||||
>
|
||||
<q-icon :name="showDetails ? 'expand_less' : 'expand_more'" left size="sm" />
|
||||
{{ showDetails ? 'Hide' : 'View' }} Details
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<div class="tw:flex tw:items-start tw:gap-4 tw:mb-6">
|
||||
<q-icon name="rocket_launch" size="2.5rem" color="primary" class="tw:flex-shrink-0" />
|
||||
<div>
|
||||
<h5 class="tw:text-lg tw:font-bold tw:m-0 tw:mb-1 title">Complete AWS Integration</h5>
|
||||
<p class="tw:text-sm tw:m-0 description">
|
||||
Deploy all selected AWS services in one click using a single CloudFormation stack.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-slide-transition>
|
||||
<div v-show="showDetails">
|
||||
<q-separator />
|
||||
<q-card-section class="tw:pt-4">
|
||||
<div class="tw:text-sm details-section">
|
||||
<h6 class="tw:text-base tw:font-semibold tw:m-0 tw:mb-3 details-title">
|
||||
Included AWS Services ({{ includedServices.length }})
|
||||
</h6>
|
||||
<!-- Deployment Mode Toggle -->
|
||||
<div class="tw:mb-6">
|
||||
<div class="step-label tw:mb-3">Deployment mode</div>
|
||||
<q-btn-toggle
|
||||
v-model="deploymentMode"
|
||||
:options="[
|
||||
{ label: 'Single Region', value: 'single' },
|
||||
{ label: 'Multi-Region (StackSets)', value: 'stackset' },
|
||||
]"
|
||||
unelevated
|
||||
toggle-color="primary"
|
||||
color="white"
|
||||
text-color="primary"
|
||||
data-test="aws-deployment-mode-toggle"
|
||||
/>
|
||||
<div class="tw:mt-2 tw:text-xs mode-hint">
|
||||
<span v-if="deploymentMode === 'single'">
|
||||
Deploys a CloudFormation stack in one AWS region. Parameters are pre-filled automatically.
|
||||
</span>
|
||||
<span v-else>
|
||||
Deploys across multiple regions using CloudFormation StackSets.
|
||||
Requires AWS Organizations or self-managed IAM roles.
|
||||
Parameters are shown for copy-paste into the AWS console wizard.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw:mb-4">
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div
|
||||
v-for="service in includedServices"
|
||||
:key="service.name"
|
||||
class="col-12 col-sm-6"
|
||||
>
|
||||
<div class="service-item tw:p-2 tw:rounded tw:border">
|
||||
<div class="tw:flex tw:items-start tw:gap-2">
|
||||
<q-icon
|
||||
:name="getCategoryIcon(service.category)"
|
||||
size="sm"
|
||||
class="tw:mt-0.5"
|
||||
color="primary"
|
||||
/>
|
||||
<div class="tw:flex-1">
|
||||
<div class="tw:font-medium tw:text-sm service-name">
|
||||
{{ service.name }}
|
||||
</div>
|
||||
<div class="tw:text-xs tw:mt-0.5 service-description">
|
||||
{{ service.description }}
|
||||
</div>
|
||||
<div class="tw:text-xs tw:mt-1 service-category">
|
||||
{{ formatCategory(service.category) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Step: Services -->
|
||||
<div class="tw:mb-6">
|
||||
<div
|
||||
class="tw:flex tw:items-center tw:justify-between tw:cursor-pointer region-collapsible-header tw:py-2 tw:px-3 tw:rounded"
|
||||
@click="showServices = !showServices"
|
||||
>
|
||||
<div class="tw:flex tw:items-center tw:gap-2">
|
||||
<q-icon :name="showServices ? 'expand_less' : 'expand_more'" color="primary" />
|
||||
<div class="step-label">Select services to monitor</div>
|
||||
<q-chip dense color="primary" text-color="white" size="sm">
|
||||
{{ enabledServices.length }} / {{ QUICK_SETUP_SERVICES.length }} selected
|
||||
</q-chip>
|
||||
</div>
|
||||
<div class="tw:flex tw:gap-2" @click.stop>
|
||||
<q-btn flat dense size="sm" color="primary" label="Select all" @click="selectAll" />
|
||||
<q-btn flat dense size="sm" color="primary" label="Deselect all" @click="deselectAll" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-slide-transition>
|
||||
<div v-show="showServices" class="tw:mt-3">
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div
|
||||
v-for="service in QUICK_SETUP_SERVICES"
|
||||
:key="service.flag"
|
||||
class="col-6 col-sm-4 col-md-3"
|
||||
>
|
||||
<q-checkbox
|
||||
v-model="enabledServices"
|
||||
:val="service.flag"
|
||||
:label="service.label"
|
||||
dense
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-slide-transition>
|
||||
</div>
|
||||
|
||||
<h6 class="tw:text-base tw:font-semibold tw:m-0 tw:mb-3 details-title">Deployment Details</h6>
|
||||
<!-- Single Region: region picker -->
|
||||
<div v-if="deploymentMode === 'single'" class="tw:mb-6">
|
||||
<div class="step-label tw:mb-3">Deployment region</div>
|
||||
<q-select
|
||||
v-model="selectedRegion"
|
||||
:options="AWS_REGIONS"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
emit-value
|
||||
map-options
|
||||
outlined
|
||||
dense
|
||||
style="max-width: 320px"
|
||||
data-test="aws-region-select"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tw:mb-3">
|
||||
<div class="tw:font-semibold tw:mb-1 detail-label">Estimated Setup Time:</div>
|
||||
<div class="detail-value">5-10 minutes for stack creation</div>
|
||||
<!-- StackSets: admin + target regions -->
|
||||
<template v-else>
|
||||
<div class="tw:mb-6">
|
||||
<div class="step-label tw:mb-3">Admin region <span class="tw:font-normal tw:text-xs region-hint">(where the StackSet is managed)</span></div>
|
||||
<q-select
|
||||
v-model="selectedRegion"
|
||||
:options="AWS_REGIONS"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
emit-value
|
||||
map-options
|
||||
outlined
|
||||
dense
|
||||
style="max-width: 320px"
|
||||
data-test="aws-admin-region-select"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tw:mb-6">
|
||||
<div
|
||||
class="tw:flex tw:items-center tw:justify-between tw:cursor-pointer region-collapsible-header tw:py-2 tw:px-3 tw:rounded"
|
||||
@click="showTargetRegions = !showTargetRegions"
|
||||
>
|
||||
<div class="tw:flex tw:items-center tw:gap-2">
|
||||
<q-icon :name="showTargetRegions ? 'expand_less' : 'expand_more'" color="primary" />
|
||||
<div class="step-label">
|
||||
Target regions
|
||||
<span class="tw:font-normal tw:text-xs region-hint">(where stacks will be deployed)</span>
|
||||
</div>
|
||||
<q-chip
|
||||
v-if="targetRegions.length > 0"
|
||||
dense color="primary" text-color="white" size="sm"
|
||||
>{{ targetRegions.length }} selected</q-chip>
|
||||
</div>
|
||||
<div class="tw:flex tw:gap-2" @click.stop>
|
||||
<q-btn flat dense size="sm" color="primary" label="Select all" @click="selectAllRegions" />
|
||||
<q-btn flat dense size="sm" color="primary" label="Clear" @click="targetRegions = []" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw:mb-3">
|
||||
<div class="tw:font-semibold tw:mb-1 detail-label">AWS Permissions Required:</div>
|
||||
<div class="detail-value">CloudFormation, IAM, Kinesis Firehose, S3</div>
|
||||
</div>
|
||||
|
||||
<div class="tw:mb-3">
|
||||
<div class="tw:font-semibold tw:mb-1 detail-label">What Gets Created:</div>
|
||||
<ul class="tw:list-disc tw:ml-5 tw:space-y-1 detail-value">
|
||||
<li>Kinesis Firehose delivery streams for each service</li>
|
||||
<li>IAM roles with appropriate permissions</li>
|
||||
<li>S3 bucket for backup (optional)</li>
|
||||
<li>CloudWatch log subscriptions</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="tw:font-semibold tw:mb-1 detail-label">Cost Considerations:</div>
|
||||
<div class="detail-value">
|
||||
Charges apply for Kinesis Firehose data transfer, CloudWatch Logs ingestion, and OpenObserve storage.
|
||||
Refer to AWS and OpenObserve pricing for details.
|
||||
<q-slide-transition>
|
||||
<div v-show="showTargetRegions" class="tw:mt-3">
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div
|
||||
v-for="region in AWS_REGIONS"
|
||||
:key="region.value"
|
||||
class="col-12 col-sm-6 col-md-4"
|
||||
>
|
||||
<q-checkbox
|
||||
v-model="targetRegions"
|
||||
:val="region.value"
|
||||
:label="`${region.label} (${region.value})`"
|
||||
dense
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-slide-transition>
|
||||
</div>
|
||||
|
||||
<div class="tw:mb-6">
|
||||
<div class="step-label tw:mb-3">Deployment model</div>
|
||||
<q-btn-toggle
|
||||
v-model="stackSetModel"
|
||||
:options="[
|
||||
{ label: 'Self-managed', value: 'self' },
|
||||
{ label: 'Service-managed (AWS Organizations)', value: 'service' },
|
||||
]"
|
||||
unelevated
|
||||
toggle-color="primary"
|
||||
color="white"
|
||||
text-color="primary"
|
||||
data-test="aws-stackset-model-toggle"
|
||||
/>
|
||||
<div class="tw:mt-2 tw:text-xs mode-hint">
|
||||
<span v-if="stackSetModel === 'self'">
|
||||
Requires <code>AWSCloudFormationStackSetAdministrationRole</code> and
|
||||
<code>AWSCloudFormationStackSetExecutionRole</code> IAM roles in your account.
|
||||
</span>
|
||||
<span v-else>
|
||||
Uses AWS Organizations. Your account must be the management or delegated admin account.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Launch -->
|
||||
<div class="tw:flex tw:items-center tw:gap-3 tw:mb-6">
|
||||
<q-btn
|
||||
color="primary"
|
||||
unelevated
|
||||
:disable="enabledServices.length === 0 || (deploymentMode === 'stackset' && targetRegions.length === 0)"
|
||||
@click="handleLaunch"
|
||||
:data-test="deploymentMode === 'single' ? 'aws-quick-setup-deploy-btn' : 'aws-stackset-launch-btn'"
|
||||
>
|
||||
<q-icon name="cloud_upload" left size="sm" />
|
||||
{{ deploymentMode === 'single' ? 'Launch CloudFormation Stack' : 'Open StackSets Console' }}
|
||||
</q-btn>
|
||||
<span v-if="enabledServices.length === 0" class="tw:text-sm text-negative">
|
||||
Select at least one service
|
||||
</span>
|
||||
<span v-else-if="deploymentMode === 'stackset' && targetRegions.length === 0" class="tw:text-sm text-negative">
|
||||
Select at least one target region
|
||||
</span>
|
||||
<span v-else class="tw:text-sm detail-value">
|
||||
{{ enabledServices.length }} service{{ enabledServices.length > 1 ? 's' : '' }} selected
|
||||
<template v-if="deploymentMode === 'stackset'"> · {{ targetRegions.length }} region{{ targetRegions.length > 1 ? 's' : '' }}</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- StackSets Parameter Helper -->
|
||||
<q-slide-transition>
|
||||
<div v-if="showParamHelper && deploymentMode === 'stackset'">
|
||||
<q-separator class="tw:mb-4" />
|
||||
<div class="param-helper">
|
||||
<div class="tw:flex tw:items-center tw:justify-between tw:mb-3">
|
||||
<div class="tw:font-semibold step-label">Parameters to enter in the AWS wizard</div>
|
||||
<q-btn flat dense size="sm" icon="close" @click="showParamHelper = false" />
|
||||
</div>
|
||||
<p class="tw:text-xs tw:mb-3 mode-hint">
|
||||
The StackSets console doesn't support URL pre-fill. Enter these values as you go through the wizard.
|
||||
</p>
|
||||
<div class="param-table">
|
||||
<div v-for="param in stackSetParams" :key="param.key" class="param-row">
|
||||
<div class="param-key">{{ param.key }}</div>
|
||||
<div class="param-value">
|
||||
<span class="param-val-text">{{ param.value }}</span>
|
||||
<q-btn
|
||||
flat dense round size="xs" icon="content_copy" color="primary"
|
||||
@click="copyParam(param.value)"
|
||||
>
|
||||
<q-tooltip>Copy</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw:mt-3">
|
||||
<div class="tw:font-semibold tw:text-xs tw:mb-1 step-label">Target regions to enter in "Deployment targets":</div>
|
||||
<div class="tw:flex tw:flex-wrap tw:gap-1 tw:mt-1">
|
||||
<q-chip
|
||||
v-for="r in targetRegions"
|
||||
:key="r"
|
||||
dense
|
||||
color="primary"
|
||||
text-color="white"
|
||||
size="sm"
|
||||
>{{ r }}</q-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-slide-transition>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -137,143 +276,170 @@ import { defineComponent, ref, computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { getEndPoint, getIngestionURL } from "@/utils/zincutils";
|
||||
import { generateCloudFormationURL, awsIntegrations } from "@/utils/awsIntegrations";
|
||||
import { generateCloudFormationURL, AWS_REGIONS, QUICK_SETUP_SERVICES } from "@/utils/awsIntegrations";
|
||||
import segment from "@/services/segment_analytics";
|
||||
|
||||
const COMPLETE_TEMPLATE_URL = 'https://openobserve-datasources-bucket.s3.us-east-2.amazonaws.com/datasource/cloud/aws/aws_complete.yaml';
|
||||
|
||||
export default defineComponent({
|
||||
name: "AWSQuickSetup",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const q = useQuasar();
|
||||
const showDetails = ref(false);
|
||||
|
||||
// Get endpoint information during setup
|
||||
const deploymentMode = ref<'single' | 'stackset'>('single');
|
||||
const stackSetModel = ref<'self' | 'service'>('self');
|
||||
const selectedRegion = ref('us-east-1');
|
||||
const targetRegions = ref<string[]>(['us-east-1']);
|
||||
const enabledServices = ref<string[]>(QUICK_SETUP_SERVICES.map(s => s.flag));
|
||||
const showParamHelper = ref(false);
|
||||
const showTargetRegions = ref(false);
|
||||
const showServices = ref(false);
|
||||
|
||||
let endpoint: any = null;
|
||||
try {
|
||||
const ingestionURL = getIngestionURL();
|
||||
endpoint = getEndPoint(ingestionURL);
|
||||
endpoint = getEndPoint(getIngestionURL());
|
||||
} catch (e) {
|
||||
console.error("Error getting endpoint during setup:", e);
|
||||
console.error("Error getting endpoint:", e);
|
||||
}
|
||||
|
||||
// Get all services with CloudFormation templates
|
||||
const includedServices = computed(() => {
|
||||
return awsIntegrations
|
||||
.filter(integration =>
|
||||
integration.cloudFormationTemplate ||
|
||||
(integration.cloudFormationTemplates && integration.cloudFormationTemplates.length > 0)
|
||||
)
|
||||
.map(integration => ({
|
||||
name: integration.displayName,
|
||||
description: integration.description,
|
||||
category: integration.category,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const getCredentials = () => {
|
||||
const organizationId = store.state?.selectedOrganization?.identifier;
|
||||
const email = store.state?.userInfo?.email;
|
||||
const passcode = store.state?.organizationData?.organizationPasscode;
|
||||
return { organizationId, email, passcode };
|
||||
};
|
||||
|
||||
const selectAll = () => { enabledServices.value = QUICK_SETUP_SERVICES.map(s => s.flag); };
|
||||
const deselectAll = () => { enabledServices.value = []; };
|
||||
const selectAllRegions = () => { targetRegions.value = AWS_REGIONS.map(r => r.value); };
|
||||
|
||||
const buildServiceFlags = () => {
|
||||
const flags: Record<string, string> = {};
|
||||
QUICK_SETUP_SERVICES.forEach(({ flag }) => {
|
||||
flags[flag] = enabledServices.value.includes(flag) ? 'true' : 'false';
|
||||
});
|
||||
return flags;
|
||||
};
|
||||
|
||||
// Parameters the user will need to copy-paste in the StackSets wizard
|
||||
const stackSetParams = computed(() => {
|
||||
const { organizationId, email, passcode } = getCredentials();
|
||||
if (!organizationId || !email || !passcode || !endpoint?.url) return [];
|
||||
|
||||
const accessKey = btoa(`${email}:${passcode}`);
|
||||
const endpointUrl = `${endpoint.url}/aws/${organizationId}/default/_kinesis_firehose`;
|
||||
const serviceFlags = buildServiceFlags();
|
||||
|
||||
const params = [
|
||||
{ key: 'Amazon S3 URL (template)', value: COMPLETE_TEMPLATE_URL },
|
||||
{ key: 'TemplateS3Bucket', value: 'openobserve-datasources-bucket' },
|
||||
{ key: 'TemplateS3Prefix', value: 'datasource/cloud/aws' },
|
||||
{ key: 'OpenObserveEndpoint', value: endpointUrl },
|
||||
{ key: 'OpenObserveAccessKey', value: accessKey },
|
||||
...QUICK_SETUP_SERVICES.map(({ flag }) => ({ key: flag, value: serviceFlags[flag] })),
|
||||
];
|
||||
return params;
|
||||
});
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
const iconMap: Record<string, string> = {
|
||||
'logs': 'description',
|
||||
'metrics': 'speed',
|
||||
'security': 'security',
|
||||
'networking': 'network_check',
|
||||
'other': 'settings',
|
||||
};
|
||||
return iconMap[category] || 'cloud';
|
||||
const copyParam = (value: string) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
q.notify({ type: 'positive', message: 'Copied to clipboard', timeout: 1500 });
|
||||
};
|
||||
|
||||
const formatCategory = (category: string) => {
|
||||
return category.charAt(0).toUpperCase() + category.slice(1);
|
||||
};
|
||||
const handleLaunch = () => {
|
||||
if (!endpoint?.url) {
|
||||
q.notify({ type: "negative", message: "Invalid ingestion endpoint. Please check configuration.", timeout: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
const handleDeployStack = () => {
|
||||
try {
|
||||
// Validate endpoint
|
||||
if (!endpoint?.url) {
|
||||
console.error("Invalid endpoint:", endpoint);
|
||||
q.notify({
|
||||
type: "negative",
|
||||
message: "Invalid ingestion endpoint. Please check configuration.",
|
||||
timeout: 3000,
|
||||
});
|
||||
const { organizationId, email, passcode } = getCredentials();
|
||||
if (!organizationId || !email || !passcode) {
|
||||
q.notify({ type: "negative", message: "Missing organization credentials. Please refresh the page.", timeout: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
if (deploymentMode.value === 'single') {
|
||||
launchSingleRegion(organizationId, email, passcode);
|
||||
} else {
|
||||
if (targetRegions.value.length === 0) {
|
||||
q.notify({ type: "warning", message: "Select at least one target region.", timeout: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
// Get organization details
|
||||
const organizationId = store.state?.selectedOrganization?.identifier;
|
||||
const email = store.state?.userInfo?.email;
|
||||
const passcode = store.state?.organizationData?.organizationPasscode;
|
||||
|
||||
// Validate required data
|
||||
if (!organizationId || !email || !passcode) {
|
||||
console.error("Missing required data:", { organizationId, email, hasPasscode: !!passcode });
|
||||
q.notify({
|
||||
type: "negative",
|
||||
message: "Missing organization credentials. Please refresh the page.",
|
||||
timeout: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate base64 encoded access key
|
||||
const accessKey = btoa(`${email}:${passcode}`);
|
||||
|
||||
// Create a unified integration object
|
||||
const unifiedIntegration = {
|
||||
id: 'aws-complete',
|
||||
name: 'AWS-Complete',
|
||||
displayName: 'AWS Complete Integration',
|
||||
cloudFormationTemplate: 'https://openobserve-datasources-bucket.s3.us-east-2.amazonaws.com/datasource/cloud/aws/aws_complete.yaml',
|
||||
comingSoon: false,
|
||||
};
|
||||
|
||||
// Generate CloudFormation URL
|
||||
const cloudFormationURL = generateCloudFormationURL(
|
||||
unifiedIntegration as any,
|
||||
organizationId,
|
||||
`${endpoint.url}/aws/${organizationId}/default/_kinesis_firehose`,
|
||||
accessKey
|
||||
);
|
||||
|
||||
if (!cloudFormationURL) {
|
||||
q.notify({
|
||||
type: "warning",
|
||||
message: "CloudFormation template not available yet",
|
||||
timeout: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Open AWS Console in new tab
|
||||
window.open(cloudFormationURL, '_blank', 'noopener,noreferrer');
|
||||
|
||||
// Track analytics
|
||||
segment.track("AWS Complete Integration Started", {
|
||||
integration_type: "complete",
|
||||
services_count: includedServices.value.length,
|
||||
});
|
||||
|
||||
q.notify({
|
||||
type: "info",
|
||||
message: "Opening AWS Console to deploy complete integration stack",
|
||||
timeout: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deploying stack:", error);
|
||||
q.notify({
|
||||
type: "negative",
|
||||
message: `Error opening AWS Console: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
timeout: 5000,
|
||||
});
|
||||
launchStackSet(organizationId);
|
||||
}
|
||||
};
|
||||
|
||||
const launchSingleRegion = (organizationId: string, email: string, passcode: string) => {
|
||||
const accessKey = btoa(`${email}:${passcode}`);
|
||||
|
||||
const url = generateCloudFormationURL(
|
||||
{
|
||||
id: 'aws-complete',
|
||||
name: 'AWS-Complete',
|
||||
displayName: 'AWS Complete Integration',
|
||||
cloudFormationTemplate: COMPLETE_TEMPLATE_URL,
|
||||
comingSoon: false,
|
||||
} as any,
|
||||
organizationId,
|
||||
`${endpoint.url}/aws/${organizationId}/default/_kinesis_firehose`,
|
||||
accessKey,
|
||||
selectedRegion.value,
|
||||
{
|
||||
TemplateS3Bucket: 'openobserve-datasources-bucket',
|
||||
TemplateS3Prefix: 'datasource/cloud/aws',
|
||||
...buildServiceFlags(),
|
||||
}
|
||||
);
|
||||
|
||||
if (!url) {
|
||||
q.notify({ type: "warning", message: "CloudFormation template not available yet", timeout: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
segment.track("AWS Complete Integration Started", { mode: 'single', region: selectedRegion.value, services: enabledServices.value });
|
||||
q.notify({ type: "info", message: "Opening AWS Console to deploy complete integration stack", timeout: 3000 });
|
||||
};
|
||||
|
||||
const launchStackSet = (organizationId: string) => {
|
||||
// StackSets console doesn't support URL pre-fill — open the console and show param helper
|
||||
const consoleUrl = `https://console.aws.amazon.com/cloudformation/home?region=${selectedRegion.value}#/stacksets/create`;
|
||||
window.open(consoleUrl, '_blank', 'noopener,noreferrer');
|
||||
showParamHelper.value = true;
|
||||
|
||||
segment.track("AWS StackSet Integration Started", {
|
||||
mode: 'stackset',
|
||||
model: stackSetModel.value,
|
||||
admin_region: selectedRegion.value,
|
||||
target_regions: targetRegions.value,
|
||||
services: enabledServices.value,
|
||||
});
|
||||
|
||||
q.notify({
|
||||
type: "info",
|
||||
message: "AWS StackSets console opened. Use the parameter values below to complete setup.",
|
||||
timeout: 5000,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
showDetails,
|
||||
includedServices,
|
||||
handleDeployStack,
|
||||
getCategoryIcon,
|
||||
formatCategory,
|
||||
deploymentMode,
|
||||
stackSetModel,
|
||||
selectedRegion,
|
||||
targetRegions,
|
||||
enabledServices,
|
||||
showParamHelper,
|
||||
showTargetRegions,
|
||||
showServices,
|
||||
stackSetParams,
|
||||
AWS_REGIONS,
|
||||
QUICK_SETUP_SERVICES,
|
||||
selectAll,
|
||||
deselectAll,
|
||||
selectAllRegions,
|
||||
copyParam,
|
||||
handleLaunch,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -286,96 +452,75 @@ export default defineComponent({
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
transition: all 0.2s ease;
|
||||
.step-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.param-helper {
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.param-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.param-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.param-key {
|
||||
min-width: 240px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.param-val-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.body--light & {
|
||||
.title {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.description,
|
||||
.benefit-text,
|
||||
.detail-value {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.service-description {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.service-category {
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
border: 1px solid #e0e0e0;
|
||||
background-color: #fafafa;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header,
|
||||
.detail-label,
|
||||
.details-title {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.notice-banner {
|
||||
background-color: #e3f2fd;
|
||||
}
|
||||
.title { color: #1a1a1a; }
|
||||
.description, .detail-value, .mode-hint { color: #666; }
|
||||
.step-label { color: #333; }
|
||||
.region-hint { color: #888; }
|
||||
.param-helper { background: #f5f5f5; }
|
||||
.param-row { background: #fff; border: 1px solid #e0e0e0; }
|
||||
.param-key { color: #333; }
|
||||
.param-val-text { color: #555; }
|
||||
.region-collapsible-header { background: #f0f4ff; border: 1px solid #d0d9f0; &:hover { background: #e8eeff; } }
|
||||
}
|
||||
|
||||
.body--dark & {
|
||||
.title {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.description,
|
||||
.benefit-text,
|
||||
.detail-value {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.service-description {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.service-category {
|
||||
color: #808080;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
border: 1px solid #404040;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.section-header,
|
||||
.detail-label,
|
||||
.details-title {
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
.notice-banner {
|
||||
background-color: rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
.title { color: #e0e0e0; }
|
||||
.description, .detail-value, .mode-hint { color: #b0b0b0; }
|
||||
.step-label { color: #d0d0d0; }
|
||||
.region-hint { color: #888; }
|
||||
.param-helper { background: rgba(255,255,255,0.05); }
|
||||
.param-row { background: rgba(255,255,255,0.03); border: 1px solid #404040; }
|
||||
.param-key { color: #ccc; }
|
||||
.param-val-text { color: #aaa; }
|
||||
.region-collapsible-header { background: rgba(255,255,255,0.06); border: 1px solid #404040; &:hover { background: rgba(255,255,255,0.09); } }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { mount, VueWrapper } from '@vue/test-utils';
|
||||
import { Quasar } from 'quasar';
|
||||
import { Quasar, Notify } from 'quasar';
|
||||
import { createStore } from 'vuex';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
@@ -14,37 +14,30 @@ vi.mock('../../../utils/zincutils', () => ({
|
||||
getImageURL: vi.fn((path) => `mock-image-url-${path}`),
|
||||
getEndPoint: vi.fn(() => ({ url: 'http://localhost:5080', host: 'localhost', port: '5080', protocol: 'http', tls: false })),
|
||||
getIngestionURL: vi.fn(() => 'http://localhost:5080'),
|
||||
b64EncodeStandard: vi.fn((str) => btoa(str)),
|
||||
maskText: vi.fn((str) => '***'),
|
||||
}));
|
||||
|
||||
vi.mock('../../../utils/azureIntegrations', () => ({
|
||||
azureIntegrations: [
|
||||
{ id: 'azure-ad', name: 'AzureAD', displayName: 'Azure Active Directory', description: 'Azure AD logs', category: 'security', hasDashboard: false },
|
||||
{ id: 'azure-storage', name: 'AzureStorage', displayName: 'Azure Storage', description: 'Azure Storage logs', category: 'storage', hasDashboard: true },
|
||||
{
|
||||
id: 'activity-logs',
|
||||
name: 'Activity Logs',
|
||||
displayName: 'Azure Activity Logs',
|
||||
description: 'Stream Azure subscription activity logs',
|
||||
armTemplate: 'https://mock-s3-bucket.s3.amazonaws.com/activity-logs.json',
|
||||
hasDashboard: false,
|
||||
category: 'logs',
|
||||
},
|
||||
],
|
||||
generateARMTemplateURL: vi.fn(() => 'https://portal.azure.com/#create/Microsoft.Template/uri/mock'),
|
||||
generateAzureDashboardURL: vi.fn(() => 'https://mock-dashboard-url'),
|
||||
}));
|
||||
|
||||
vi.mock('@/services/dashboards', () => ({
|
||||
default: {
|
||||
list_Folders: vi.fn(() => Promise.resolve({ data: { list: [] } })),
|
||||
new_Folder: vi.fn(() => Promise.resolve({ data: { folderId: 'folder-1', name: 'Microsoft' } })),
|
||||
list: vi.fn(() => Promise.resolve({ data: { dashboards: [] } })),
|
||||
create: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
delete: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/services/segment_analytics', () => ({
|
||||
default: { track: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('./AzureQuickSetup.vue', () => ({
|
||||
default: {
|
||||
name: 'AzureQuickSetup',
|
||||
template: '<div data-test="azure-quick-setup">Azure Quick Setup</div>',
|
||||
},
|
||||
}));
|
||||
|
||||
const mockStore = createStore({
|
||||
state: {
|
||||
selectedOrganization: { identifier: 'test-org', name: 'Test Organization' },
|
||||
@@ -64,20 +57,21 @@ describe('AzureConfig.vue', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubGlobal('open', vi.fn());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) wrapper.unmount();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const createWrapper = () => {
|
||||
return mount(AzureConfig, {
|
||||
const createWrapper = () =>
|
||||
mount(AzureConfig, {
|
||||
global: {
|
||||
plugins: [Quasar, mockI18n, mockRouter],
|
||||
plugins: [[Quasar, { plugins: { Notify } }], mockI18n, mockRouter],
|
||||
provide: { store: mockStore },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('Component Rendering', () => {
|
||||
it('should mount without errors', () => {
|
||||
@@ -90,16 +84,20 @@ describe('AzureConfig.vue', () => {
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it('should render AzureQuickSetup child component', () => {
|
||||
wrapper = createWrapper();
|
||||
const child = wrapper.find('[data-test="azure-quick-setup"]');
|
||||
expect(child.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have azure-config-page class', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(wrapper.find('.azure-config-page').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render Azure Activity Logs title', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(wrapper.text()).toContain('Azure Activity Logs');
|
||||
});
|
||||
|
||||
it('should render deploy button', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(wrapper.find('[data-test="azure-activity-logs-deploy-btn"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component Identity', () => {
|
||||
@@ -114,4 +112,39 @@ describe('AzureConfig.vue', () => {
|
||||
expect(typeof wrapper.vm).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Step 2 Mode Toggle', () => {
|
||||
it('should default to portal mode', () => {
|
||||
wrapper = createWrapper();
|
||||
expect((wrapper.vm as any).step2Mode).toBe('portal');
|
||||
});
|
||||
|
||||
it('should switch to cli mode', async () => {
|
||||
wrapper = createWrapper();
|
||||
(wrapper.vm as any).step2Mode = 'cli';
|
||||
await wrapper.vm.$nextTick();
|
||||
expect((wrapper.vm as any).step2Mode).toBe('cli');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Category Selection', () => {
|
||||
it('should start with all categories enabled', () => {
|
||||
wrapper = createWrapper();
|
||||
expect((wrapper.vm as any).enabledCategories.length).toBe(8);
|
||||
});
|
||||
|
||||
it('should clear categories', () => {
|
||||
wrapper = createWrapper();
|
||||
(wrapper.vm as any).enabledCategories = [];
|
||||
expect((wrapper.vm as any).enabledCategories.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDeploy', () => {
|
||||
it('should call window.open when credentials are valid', () => {
|
||||
wrapper = createWrapper();
|
||||
(wrapper.vm as any).handleDeploy();
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,34 +16,294 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<template>
|
||||
<div class="q-ma-md azure-config-page">
|
||||
<AzureQuickSetup />
|
||||
|
||||
<!-- Header -->
|
||||
<div class="tw:flex tw:items-start tw:gap-4 tw:mb-6">
|
||||
<q-icon name="cloud" size="2.5rem" color="primary" class="tw:flex-shrink-0" />
|
||||
<div>
|
||||
<h5 class="tw:text-lg tw:font-bold tw:m-0 tw:mb-1 title">Azure Activity Logs</h5>
|
||||
<p class="tw:text-sm tw:m-0 page-description">
|
||||
Stream Azure subscription activity logs to OpenObserve via Event Hub.
|
||||
The ARM template sets up the Event Hub infrastructure — you then configure Azure to export logs to it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1 -->
|
||||
<div class="step-card tw:mb-4 tw:p-4 tw:rounded">
|
||||
<div style="display: grid; grid-template-columns: 28px 1fr; gap: 12px; align-items: start;">
|
||||
<div class="step-number">1</div>
|
||||
<div>
|
||||
<div class="tw:font-semibold tw:mb-1 step-title">Deploy ARM Template</div>
|
||||
<p class="tw:text-sm tw:m-0 tw:mb-3 step-desc">
|
||||
Creates an Event Hub namespace, Event Hub, and all required resources in your Azure subscription.
|
||||
</p>
|
||||
<q-btn
|
||||
color="primary"
|
||||
unelevated
|
||||
@click="handleDeploy"
|
||||
data-test="azure-activity-logs-deploy-btn"
|
||||
>
|
||||
<q-icon name="rocket_launch" left size="sm" />
|
||||
Deploy to Azure
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="step-card tw:mb-4 tw:p-4 tw:rounded">
|
||||
<div style="display: grid; grid-template-columns: 28px 1fr; gap: 12px; align-items: start;">
|
||||
<div class="step-number">2</div>
|
||||
<div>
|
||||
<div class="tw:font-semibold tw:mb-1 step-title">Configure Diagnostic Settings</div>
|
||||
<p class="tw:text-sm tw:mb-3 step-desc">
|
||||
After the ARM deployment completes, route Activity Logs to the Event Hub that was created.
|
||||
</p>
|
||||
|
||||
<!-- Portal / CLI toggle -->
|
||||
<q-btn-toggle
|
||||
v-model="step2Mode"
|
||||
:options="[
|
||||
{ label: 'Azure Portal', value: 'portal' },
|
||||
{ label: 'Azure CLI', value: 'cli' },
|
||||
]"
|
||||
unelevated
|
||||
toggle-color="primary"
|
||||
color="white"
|
||||
text-color="primary"
|
||||
size="sm"
|
||||
class="tw:mb-4"
|
||||
/>
|
||||
|
||||
<!-- Portal instructions -->
|
||||
<div v-if="step2Mode === 'portal'">
|
||||
<ol class="tw:text-sm tw:pl-4 tw:space-y-1 step-desc">
|
||||
<li>Go to <strong>Azure Portal → Subscriptions → your subscription</strong></li>
|
||||
<li>Click <strong>Activity log</strong> in the left menu</li>
|
||||
<li>Click <strong>Export Activity Logs</strong> (or <strong>Diagnostic settings → + Add diagnostic setting</strong>)</li>
|
||||
<li>Enter a name, check the log categories you want to enable</li>
|
||||
<li>Under <strong>Destination details</strong>, choose <strong>Stream to an event hub</strong></li>
|
||||
<li>Select the Event Hub namespace and Event Hub created in Step 1 (prefix: <code>o2-activity</code>)</li>
|
||||
<li>Click <strong>Save</strong></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- CLI: inputs + generated curl command -->
|
||||
<div v-else>
|
||||
<!-- Categories -->
|
||||
<div class="tw:mb-4">
|
||||
<div class="tw:flex tw:items-center tw:justify-between tw:mb-2">
|
||||
<div class="tw:text-xs tw:font-semibold section-label">Log categories to enable</div>
|
||||
<div class="tw:flex tw:gap-2">
|
||||
<q-btn flat dense size="sm" color="primary" label="Select all" @click="enabledCategories = LOG_CATEGORIES.map(c => c.value)" />
|
||||
<q-btn flat dense size="sm" color="primary" label="Clear" @click="enabledCategories = []" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; width: 100%;">
|
||||
<q-checkbox
|
||||
v-for="cat in LOG_CATEGORIES"
|
||||
:key="cat.value"
|
||||
v-model="enabledCategories"
|
||||
:val="cat.value"
|
||||
:label="cat.label"
|
||||
dense
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
|
||||
<div>
|
||||
<div class="tw:text-xs tw:mb-1 section-label">Resource Group</div>
|
||||
<q-input
|
||||
v-model="resourceGroup"
|
||||
outlined
|
||||
dense
|
||||
hide-bottom-space
|
||||
placeholder="rg-openobserve-activity-logs"
|
||||
autocomplete="off"
|
||||
data-test="azure-resource-group-input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="tw:text-xs tw:mb-1 section-label">Deployment Name</div>
|
||||
<q-input
|
||||
v-model="deploymentName"
|
||||
outlined
|
||||
dense
|
||||
hide-bottom-space
|
||||
placeholder="o2-activity-20260420"
|
||||
autocomplete="off"
|
||||
data-test="azure-deployment-name-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="enabledCategories.length === 0" class="tw:text-sm text-negative tw:mb-3">
|
||||
Select at least one log category above.
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="tw:text-xs tw:mb-2 step-desc">Run this command after your ARM deployment completes:</p>
|
||||
<CopyContent :content="curlCommand" data-test="azure-curl-command" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Configuration -->
|
||||
<div class="tw:mt-6">
|
||||
<div class="tw:font-semibold tw:text-sm tw:mb-2 section-label">Manual Configuration (for reference)</div>
|
||||
<CopyContent :content="manualContent" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import AzureQuickSetup from "./AzureQuickSetup.vue";
|
||||
import { defineComponent, computed, ref } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { getEndPoint, getIngestionURL } from "@/utils/zincutils";
|
||||
import { generateARMTemplateURL, azureIntegrations } from "@/utils/azureIntegrations";
|
||||
import CopyContent from "@/components/CopyContent.vue";
|
||||
import segment from "@/services/segment_analytics";
|
||||
|
||||
const SCRIPT_URL = 'https://raw.githubusercontent.com/openobserve/o2-datasource/main/azure/azure_activity_logs/configure-diagnostic-settings.sh';
|
||||
|
||||
const LOG_CATEGORIES = [
|
||||
{ value: 'Administrative', label: 'Administrative' },
|
||||
{ value: 'Security', label: 'Security' },
|
||||
{ value: 'ServiceHealth', label: 'Service Health' },
|
||||
{ value: 'Alert', label: 'Alert' },
|
||||
{ value: 'Recommendation', label: 'Recommendation' },
|
||||
{ value: 'Policy', label: 'Policy' },
|
||||
{ value: 'Autoscale', label: 'Autoscale' },
|
||||
{ value: 'ResourceHealth', label: 'Resource Health' },
|
||||
];
|
||||
|
||||
const activityLogsIntegration = azureIntegrations.find(i => i.id === 'activity-logs')!;
|
||||
|
||||
export default defineComponent({
|
||||
name: "AzureConfig",
|
||||
components: {
|
||||
AzureQuickSetup,
|
||||
components: { CopyContent },
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const q = useQuasar();
|
||||
|
||||
let endpoint: any = null;
|
||||
try {
|
||||
endpoint = getEndPoint(getIngestionURL());
|
||||
} catch (e) {
|
||||
console.error("Error getting endpoint:", e);
|
||||
}
|
||||
|
||||
const step2Mode = ref<'portal' | 'cli'>('portal');
|
||||
const enabledCategories = ref<string[]>(LOG_CATEGORIES.map(c => c.value));
|
||||
const resourceGroup = ref('');
|
||||
const deploymentName = ref('');
|
||||
|
||||
const curlCommand = computed(() => {
|
||||
const rg = resourceGroup.value || 'YOUR-RESOURCE-GROUP';
|
||||
const dn = deploymentName.value || 'YOUR-DEPLOYMENT-NAME';
|
||||
const cats = enabledCategories.value.join(',');
|
||||
return `curl -s ${SCRIPT_URL} | bash -s -- \\
|
||||
--resource-group "${rg}" \\
|
||||
--deployment-name "${dn}" \\
|
||||
--categories "${cats}"`;
|
||||
});
|
||||
|
||||
const manualContent = computed(() => {
|
||||
const orgId = store.state?.selectedOrganization?.identifier || '';
|
||||
const url = endpoint?.url || '';
|
||||
return `Event Hub → OpenObserve Endpoint: ${url}/azure/${orgId}/default/_event_hub\nAccess Key: [BASIC_PASSCODE]`;
|
||||
});
|
||||
|
||||
const handleDeploy = () => {
|
||||
if (!endpoint?.url) {
|
||||
q.notify({ type: "negative", message: "Invalid ingestion endpoint. Please check configuration.", timeout: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
const organizationId = store.state?.selectedOrganization?.identifier;
|
||||
const email = store.state?.userInfo?.email;
|
||||
const passcode = store.state?.organizationData?.organizationPasscode;
|
||||
|
||||
if (!organizationId || !email || !passcode) {
|
||||
q.notify({ type: "negative", message: "Missing organization credentials. Please refresh the page.", timeout: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
const accessKey = btoa(`${email}:${passcode}`);
|
||||
const endpointUrl = `${endpoint.url}/azure/${organizationId}/default/_event_hub`;
|
||||
|
||||
const url = generateARMTemplateURL(activityLogsIntegration, endpointUrl, accessKey);
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
|
||||
segment.track("Azure Activity Logs Deploy Started", { integration_id: 'activity-logs' });
|
||||
|
||||
q.notify({ type: "info", message: "Opening Azure portal to deploy Activity Logs infrastructure", timeout: 3000 });
|
||||
};
|
||||
|
||||
return {
|
||||
LOG_CATEGORIES,
|
||||
step2Mode,
|
||||
enabledCategories,
|
||||
resourceGroup,
|
||||
deploymentName,
|
||||
curlCommand,
|
||||
manualContent,
|
||||
handleDeploy,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.azure-config-page {
|
||||
max-width: 860px;
|
||||
|
||||
.step-card {
|
||||
border-left: 3px solid;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Suppress Quasar's focus container highlight for inputs in this component
|
||||
:deep(.q-field--outlined.q-field--highlighted .q-field__control:after) {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
:deep(.q-field--outlined.q-field--highlighted .q-field__control:before) {
|
||||
border-color: rgba(0, 0, 0, 0.24) !important;
|
||||
}
|
||||
|
||||
.body--light & {
|
||||
.page-description {
|
||||
color: #666;
|
||||
}
|
||||
.title { color: #1a1a1a; }
|
||||
.page-description, .step-desc { color: #666; }
|
||||
.section-label { color: #333; }
|
||||
.step-card { background: #fafafa; border-color: #e0e0e0; }
|
||||
.step-title { color: #1a1a1a; }
|
||||
.step-number { background: #1976d2; color: #fff; }
|
||||
}
|
||||
|
||||
.body--dark & {
|
||||
.page-description {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
.title { color: #e0e0e0; }
|
||||
.page-description, .step-desc { color: #b0b0b0; }
|
||||
.section-label { color: #d0d0d0; }
|
||||
.step-card { background: rgba(255,255,255,0.04); border-color: #404040; }
|
||||
.step-title { color: #e0e0e0; }
|
||||
.step-number { background: #1976d2; color: #fff; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,6 +43,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
data-test="azure-integration-category-tabs"
|
||||
>
|
||||
<q-tab name="all" label="All Services" />
|
||||
<q-tab name="logs" label="Logs" />
|
||||
<q-tab name="compute" label="Compute" />
|
||||
<q-tab name="storage" label="Storage" />
|
||||
<q-tab name="security" label="Security" />
|
||||
|
||||
@@ -46,9 +46,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions class="tw:px-4 tw:pb-4 tw:flex tw:flex-row tw:gap-2">
|
||||
<!-- Documentation Button -->
|
||||
<!-- Deploy Button (ARM template) -->
|
||||
<q-btn
|
||||
v-if="integration.documentationUrl"
|
||||
v-if="integration.armTemplate"
|
||||
color="primary"
|
||||
label="Deploy"
|
||||
@click="handleDeploy()"
|
||||
unelevated
|
||||
class="tw:flex-1"
|
||||
:data-test="`azure-${integration.id}-deploy-btn`"
|
||||
/>
|
||||
<!-- Documentation Button (shown when no ARM template) -->
|
||||
<q-btn
|
||||
v-else-if="integration.documentationUrl"
|
||||
color="primary"
|
||||
label="Documentation"
|
||||
@click="handleDocumentation()"
|
||||
@@ -56,6 +66,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
class="tw:flex-1"
|
||||
:data-test="`azure-${integration.id}-documentation-btn`"
|
||||
/>
|
||||
<!-- Docs icon button (shown alongside Deploy when both exist) -->
|
||||
<q-btn
|
||||
v-if="integration.armTemplate && integration.documentationUrl"
|
||||
flat
|
||||
dense
|
||||
round
|
||||
size="sm"
|
||||
icon="description"
|
||||
color="primary"
|
||||
@click="handleDocumentation()"
|
||||
:data-test="`azure-${integration.id}-docs-icon-btn`"
|
||||
>
|
||||
<q-tooltip>View Documentation</q-tooltip>
|
||||
</q-btn>
|
||||
<!-- Dashboard Button -->
|
||||
<q-btn
|
||||
outline
|
||||
@@ -77,7 +101,8 @@ import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useRouter } from "vue-router";
|
||||
import type { AzureIntegration } from "@/utils/azureIntegrations";
|
||||
import { generateAzureDashboardURL } from "@/utils/azureIntegrations";
|
||||
import { generateAzureDashboardURL, generateARMTemplateURL } from "@/utils/azureIntegrations";
|
||||
import { getEndPoint, getIngestionURL } from "@/utils/zincutils";
|
||||
import segment from "@/services/segment_analytics";
|
||||
|
||||
export default defineComponent({
|
||||
@@ -93,6 +118,45 @@ export default defineComponent({
|
||||
const q = useQuasar();
|
||||
const router = useRouter();
|
||||
|
||||
let endpoint: any = null;
|
||||
try {
|
||||
endpoint = getEndPoint(getIngestionURL());
|
||||
} catch (e) {
|
||||
console.error("Error getting endpoint:", e);
|
||||
}
|
||||
|
||||
const handleDeploy = () => {
|
||||
if (!props.integration.armTemplate) return;
|
||||
|
||||
if (!endpoint?.url) {
|
||||
q.notify({ type: "negative", message: "Invalid ingestion endpoint. Please check configuration.", timeout: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
const organizationId = store.state?.selectedOrganization?.identifier;
|
||||
const email = store.state?.userInfo?.email;
|
||||
const passcode = store.state?.organizationData?.organizationPasscode;
|
||||
|
||||
if (!organizationId || !email || !passcode) {
|
||||
q.notify({ type: "negative", message: "Missing organization credentials. Please refresh the page.", timeout: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
const accessKey = btoa(`${email}:${passcode}`);
|
||||
const endpointUrl = `${endpoint.url}/azure/${organizationId}/default/_event_hub`;
|
||||
|
||||
const url = generateARMTemplateURL(props.integration, endpointUrl, accessKey);
|
||||
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
|
||||
segment.track("Azure ARM Template Deploy Started", {
|
||||
service: props.integration.name,
|
||||
integration_id: props.integration.id,
|
||||
});
|
||||
|
||||
q.notify({ type: "info", message: `Opening Azure portal to deploy ${props.integration.displayName}`, timeout: 3000 });
|
||||
};
|
||||
|
||||
const handleDashboard = () => {
|
||||
if (!props.integration.hasDashboard) {
|
||||
return;
|
||||
@@ -146,6 +210,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
return {
|
||||
handleDeploy,
|
||||
handleDashboard,
|
||||
handleDocumentation,
|
||||
};
|
||||
|
||||
@@ -345,6 +345,59 @@ export const awsIntegrations: AWSIntegration[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const AWS_REGIONS = [
|
||||
{ value: 'us-east-1', label: 'US East (N. Virginia)' },
|
||||
{ value: 'us-east-2', label: 'US East (Ohio)' },
|
||||
{ value: 'us-west-1', label: 'US West (N. California)' },
|
||||
{ value: 'us-west-2', label: 'US West (Oregon)' },
|
||||
{ value: 'ca-central-1', label: 'Canada (Central)' },
|
||||
{ value: 'ca-west-1', label: 'Canada West (Calgary)' },
|
||||
{ value: 'eu-west-1', label: 'Europe (Ireland)' },
|
||||
{ value: 'eu-west-2', label: 'Europe (London)' },
|
||||
{ value: 'eu-west-3', label: 'Europe (Paris)' },
|
||||
{ value: 'eu-central-1', label: 'Europe (Frankfurt)' },
|
||||
{ value: 'eu-central-2', label: 'Europe (Zurich)' },
|
||||
{ value: 'eu-north-1', label: 'Europe (Stockholm)' },
|
||||
{ value: 'eu-south-1', label: 'Europe (Milan)' },
|
||||
{ value: 'eu-south-2', label: 'Europe (Spain)' },
|
||||
{ value: 'ap-east-1', label: 'Asia Pacific (Hong Kong)' },
|
||||
{ value: 'ap-south-1', label: 'Asia Pacific (Mumbai)' },
|
||||
{ value: 'ap-south-2', label: 'Asia Pacific (Hyderabad)' },
|
||||
{ value: 'ap-southeast-1', label: 'Asia Pacific (Singapore)' },
|
||||
{ value: 'ap-southeast-2', label: 'Asia Pacific (Sydney)' },
|
||||
{ value: 'ap-southeast-3', label: 'Asia Pacific (Jakarta)' },
|
||||
{ value: 'ap-southeast-4', label: 'Asia Pacific (Melbourne)' },
|
||||
{ value: 'ap-northeast-1', label: 'Asia Pacific (Tokyo)' },
|
||||
{ value: 'ap-northeast-2', label: 'Asia Pacific (Seoul)' },
|
||||
{ value: 'ap-northeast-3', label: 'Asia Pacific (Osaka)' },
|
||||
{ value: 'sa-east-1', label: 'South America (São Paulo)' },
|
||||
{ value: 'me-south-1', label: 'Middle East (Bahrain)' },
|
||||
{ value: 'me-central-1', label: 'Middle East (UAE)' },
|
||||
{ value: 'il-central-1', label: 'Israel (Tel Aviv)' },
|
||||
{ value: 'af-south-1', label: 'Africa (Cape Town)' },
|
||||
{ value: 'us-gov-east-1', label: 'AWS GovCloud (US-East)' },
|
||||
{ value: 'us-gov-west-1', label: 'AWS GovCloud (US-West)' },
|
||||
];
|
||||
|
||||
export const QUICK_SETUP_SERVICES: { label: string; flag: string }[] = [
|
||||
{ label: 'CloudTrail', flag: 'EnableCloudTrail' },
|
||||
{ label: 'CloudWatch Logs', flag: 'EnableCloudWatchLogs' },
|
||||
{ label: 'CloudWatch Metrics', flag: 'EnableCloudWatchMetrics' },
|
||||
{ label: 'VPC Flow Logs', flag: 'EnableVPCFlowLogs' },
|
||||
{ label: 'WAF', flag: 'EnableWAF' },
|
||||
{ label: 'ALB', flag: 'EnableALB' },
|
||||
{ label: 'API Gateway', flag: 'EnableApiGateway' },
|
||||
{ label: 'RDS', flag: 'EnableRDS' },
|
||||
{ label: 'S3 Access Logs', flag: 'EnableS3AccessLogs' },
|
||||
{ label: 'CloudFront', flag: 'EnableCloudFront' },
|
||||
{ label: 'Route53', flag: 'EnableRoute53' },
|
||||
{ label: 'DynamoDB', flag: 'EnableDynamoDB' },
|
||||
{ label: 'EC2', flag: 'EnableEC2' },
|
||||
{ label: 'EventBridge', flag: 'EnableEventBridge' },
|
||||
{ label: 'Kinesis', flag: 'EnableKinesisStream' },
|
||||
{ label: 'Cognito', flag: 'EnableCognito' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Generate AWS CloudFormation console URL with pre-filled parameters
|
||||
*/
|
||||
@@ -353,12 +406,13 @@ export const generateCloudFormationURL = (
|
||||
organizationId: string,
|
||||
endpoint: string,
|
||||
accessKey: string,
|
||||
region: string = 'us-east-1',
|
||||
extraParams: Record<string, string> = {},
|
||||
): string => {
|
||||
if (!integration.cloudFormationTemplate || integration.comingSoon) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const region = 'us-east-1'; // Default region, can be made configurable
|
||||
const stackName = `o2-${integration.name.replace(/\s+/g, '-')}`;
|
||||
|
||||
// Encode parameters for URL
|
||||
@@ -372,6 +426,7 @@ export const generateCloudFormationURL = (
|
||||
param_OrganizationId: organizationId,
|
||||
param_HttpEndpointUrl: endpoint,
|
||||
param_AccessKey: accessKey,
|
||||
...Object.fromEntries(Object.entries(extraParams).map(([k, v]) => [`param_${k}`, v])),
|
||||
});
|
||||
|
||||
return `https://console.aws.amazon.com/cloudformation/home?region=${region}#/stacks/create/review?${params.toString()}`;
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface AzureIntegration {
|
||||
displayName: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
armTemplate?: string; // HTTPS URL to ARM template JSON on S3
|
||||
hasDashboard: boolean;
|
||||
dashboardFolderId?: string;
|
||||
dashboardGithubUrl?: string;
|
||||
@@ -27,6 +28,17 @@ export interface AzureIntegration {
|
||||
}
|
||||
|
||||
export const azureIntegrations: AzureIntegration[] = [
|
||||
{
|
||||
id: 'activity-logs',
|
||||
name: 'Activity Logs',
|
||||
displayName: 'Azure Activity Logs',
|
||||
icon: '',
|
||||
description: 'Stream Azure subscription activity logs to OpenObserve via Event Hub and Event Publisher',
|
||||
armTemplate: 'https://openobserve-datasources-bucket.s3.us-east-2.amazonaws.com/datasource/cloud/azure/activity-logs-to-openobserve.json',
|
||||
hasDashboard: false,
|
||||
documentationUrl: 'https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log',
|
||||
category: 'logs',
|
||||
},
|
||||
{
|
||||
id: 'aks',
|
||||
name: 'AKS',
|
||||
@@ -189,6 +201,27 @@ export const azureIntegrations: AzureIntegration[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Generate Azure portal custom deployment URL with pre-filled parameters.
|
||||
* Uses the ARM template quick-deploy format:
|
||||
* https://portal.azure.com/#create/Microsoft.Template/uri/{encoded-template-url}/parameters/{encoded-params}
|
||||
*/
|
||||
export const generateARMTemplateURL = (
|
||||
integration: AzureIntegration,
|
||||
endpoint: string,
|
||||
accessKey: string,
|
||||
): string => {
|
||||
if (!integration.armTemplate) return '';
|
||||
|
||||
const templateUri = encodeURIComponent(integration.armTemplate);
|
||||
const parameters = encodeURIComponent(JSON.stringify({
|
||||
openObserveEndpoint: { value: endpoint },
|
||||
openObserveAccessKey: { value: accessKey },
|
||||
}));
|
||||
|
||||
return `https://portal.azure.com/#create/Microsoft.Template/uri/${templateUri}/parameters/${parameters}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate dashboard URL for the current environment
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user