mirror of
https://github.com/taylanbakircioglu/flowfish.git
synced 2026-09-12 05:48:55 +00:00
feat: AI Hub UX improvements and theme consistency
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { Button, message, theme } from 'antd';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
|
||||
const { useToken } = theme;
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
label: string;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({ code, label, maxHeight }) => {
|
||||
const { token } = useToken();
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
message.success(`${label} copied`);
|
||||
} catch {
|
||||
message.error('Clipboard unavailable');
|
||||
}
|
||||
};
|
||||
|
||||
if (!code) {
|
||||
return (
|
||||
<pre
|
||||
style={{
|
||||
background: token.colorBgLayout,
|
||||
color: token.colorTextSecondary,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
margin: 0,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Configure search parameters in the previous step to generate this snippet.
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={copy}
|
||||
style={{ position: 'absolute', top: 8, right: 8, zIndex: 1, opacity: 0.85 }}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
<pre
|
||||
style={{
|
||||
background: token.colorBgLayout,
|
||||
color: token.colorText,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
overflow: 'auto',
|
||||
fontSize: 12,
|
||||
margin: 0,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
maxHeight: maxHeight,
|
||||
}}
|
||||
>
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeBlock;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { Card, Table, Tag, Row, Col, Statistic, Tooltip, Empty, Typography, theme } from 'antd';
|
||||
import type { DependencySummaryService, DependencySummaryGroup } from '../../store/api/communicationApi';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
database: 'blue',
|
||||
cache: 'green',
|
||||
message_broker: 'purple',
|
||||
};
|
||||
|
||||
interface DependencyCategoryGroupProps {
|
||||
group: DependencySummaryGroup;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const DependencyCategoryGroup: React.FC<DependencyCategoryGroupProps> = ({ group, title }) => {
|
||||
const { token } = theme.useToken();
|
||||
if (!group || group.total === 0) {
|
||||
return <Empty description={`No ${title.toLowerCase()}`} image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={16} style={{ marginBottom: 12 }}>
|
||||
<Col><Statistic title="Total" value={group.total} valueStyle={{ fontSize: 20 }} /></Col>
|
||||
{(group.critical_count ?? 0) > 0 && (
|
||||
<Col><Statistic title="Critical" value={group.critical_count} valueStyle={{ fontSize: 20, color: token.colorError }} /></Col>
|
||||
)}
|
||||
<Col><Statistic title="Categories" value={Object.keys(group.by_category || {}).length} valueStyle={{ fontSize: 20 }} /></Col>
|
||||
</Row>
|
||||
{Object.entries(group.by_category || {}).map(([cat, services]: [string, DependencySummaryService[]]) => (
|
||||
<Card
|
||||
key={cat}
|
||||
size="small"
|
||||
title={
|
||||
<>
|
||||
<Tag color={CATEGORY_COLORS[cat] || 'default'}>{cat}</Tag>
|
||||
<Text type="secondary">({services.length})</Text>
|
||||
</>
|
||||
}
|
||||
style={{ marginBottom: 8 }}
|
||||
>
|
||||
<Table
|
||||
dataSource={services}
|
||||
rowKey={(r) => `${r.namespace}/${r.name}`}
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (v: string, r: DependencySummaryService) => (
|
||||
<>
|
||||
<Text strong>{v}</Text>
|
||||
{r.is_critical && <Tag color="red" style={{ marginLeft: 4 }}>critical</Tag>}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ title: 'Namespace', dataIndex: 'namespace', key: 'ns' },
|
||||
{ title: 'Kind', dataIndex: 'kind', key: 'kind', render: (v: string) => v ? <Tag>{v}</Tag> : '-' },
|
||||
{ title: 'Port', dataIndex: 'port', key: 'port', render: (v: number) => v ?? '-' },
|
||||
{
|
||||
title: 'Annotations',
|
||||
key: 'ann',
|
||||
render: (_: unknown, r: DependencySummaryService) => {
|
||||
const entries = Object.entries(r.annotations || {});
|
||||
if (!entries.length) return <Text type="secondary">-</Text>;
|
||||
const gitRepo = r.annotations['git-repo'] || r.annotations['gitRepo'] || r.annotations['source-repo'];
|
||||
if (gitRepo) return <Tooltip title={gitRepo}><Tag color="geekblue">git-repo</Tag></Tooltip>;
|
||||
return (
|
||||
<Tooltip title={entries.map(([k, v]) => `${k}=${v}`).join(', ')}>
|
||||
<Tag>{entries.length} annotations</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DependencyCategoryGroup;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,9 @@ import {
|
||||
InfoCircleOutlined,
|
||||
FileTextOutlined,
|
||||
SettingOutlined,
|
||||
RobotOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useGetClustersQuery } from '../store/api/clusterApi';
|
||||
import { useGetAnalysesQuery } from '../store/api/analysisApi';
|
||||
import { colors } from '../styles/colors';
|
||||
@@ -429,10 +431,13 @@ const BlastRadiusOracle: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Copy code to clipboard
|
||||
const copyCode = (code: string) => {
|
||||
navigator.clipboard.writeText(code);
|
||||
message.success('Code copied to clipboard!');
|
||||
const copyCode = async (code: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
message.success('Code copied to clipboard!');
|
||||
} catch {
|
||||
message.error('Clipboard unavailable');
|
||||
}
|
||||
};
|
||||
|
||||
// View assessment detail
|
||||
@@ -747,6 +752,21 @@ const BlastRadiusOracle: React.FC = () => {
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card size="small" bordered style={{ marginTop: 16 }}>
|
||||
<Space>
|
||||
<RobotOutlined />
|
||||
<div>
|
||||
<Text strong>Need dependency data for AI agents?</Text>
|
||||
<br />
|
||||
<Link to="/integration/ai-hub">
|
||||
<Text type="secondary">
|
||||
AI Integration Hub — generate integration snippets for dependency analysis
|
||||
</Text>
|
||||
</Link>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</TabPane>
|
||||
@@ -801,14 +821,15 @@ const BlastRadiusOracle: React.FC = () => {
|
||||
}
|
||||
>
|
||||
<pre style={{
|
||||
background: '#1e1e1e',
|
||||
color: '#d4d4d4',
|
||||
background: token.colorBgLayout,
|
||||
color: token.colorText,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
overflow: 'auto',
|
||||
maxHeight: 500,
|
||||
margin: 0,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}>
|
||||
{codeSnippets[selectedPlatform as keyof typeof codeSnippets]}
|
||||
</pre>
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
import type { DependencySummaryParams } from '../store/api/communicationApi';
|
||||
|
||||
const getApiBase = () =>
|
||||
typeof window !== 'undefined' ? `${window.location.origin}/api/v1` : '/api/v1';
|
||||
|
||||
const getBaseUrl = () =>
|
||||
typeof window !== 'undefined' ? window.location.origin : 'https://your-flowfish-instance';
|
||||
|
||||
export function buildQueryString(params: DependencySummaryParams | null): string {
|
||||
if (!params) return '';
|
||||
const qs = new URLSearchParams();
|
||||
params.analysis_ids.forEach(id => qs.append('analysis_ids', String(id)));
|
||||
if (params.annotation_key) qs.set('annotation_key', params.annotation_key);
|
||||
if (params.annotation_value) qs.set('annotation_value', params.annotation_value);
|
||||
if (params.label_key) qs.set('label_key', params.label_key);
|
||||
if (params.label_value) qs.set('label_value', params.label_value);
|
||||
if (params.namespace) qs.set('namespace', params.namespace);
|
||||
if (params.owner_name) qs.set('owner_name', params.owner_name);
|
||||
if (params.pod_name) qs.set('pod_name', params.pod_name);
|
||||
if (params.ip) qs.set('ip', params.ip);
|
||||
if (params.depth && params.depth > 1) qs.set('depth', String(params.depth));
|
||||
return qs.toString();
|
||||
}
|
||||
|
||||
export function buildCurlSnippet(params: DependencySummaryParams | null): string {
|
||||
const qsStr = buildQueryString(params);
|
||||
if (!qsStr) return '';
|
||||
const API_BASE = getApiBase();
|
||||
return `# Get your API key from Settings > API Keys
|
||||
FLOWFISH_API_KEY='**********'
|
||||
|
||||
curl -sf -H "X-API-Key: $FLOWFISH_API_KEY" \\
|
||||
"${API_BASE}/communications/dependencies/summary?${qsStr}"`;
|
||||
}
|
||||
|
||||
export function buildPythonSnippet(params: DependencySummaryParams | null): string {
|
||||
if (!params) return '';
|
||||
const API_BASE = getApiBase();
|
||||
const paramLines: string[] = [];
|
||||
params.analysis_ids.forEach(id => paramLines.push(` ("analysis_ids", "${id}"),`));
|
||||
if (params.annotation_key) paramLines.push(` ("annotation_key", "${params.annotation_key}"),`);
|
||||
if (params.annotation_value) paramLines.push(` ("annotation_value", "${params.annotation_value}"),`);
|
||||
if (params.label_key) paramLines.push(` ("label_key", "${params.label_key}"),`);
|
||||
if (params.label_value) paramLines.push(` ("label_value", "${params.label_value}"),`);
|
||||
if (params.namespace) paramLines.push(` ("namespace", "${params.namespace}"),`);
|
||||
if (params.owner_name) paramLines.push(` ("owner_name", "${params.owner_name}"),`);
|
||||
if (params.pod_name) paramLines.push(` ("pod_name", "${params.pod_name}"),`);
|
||||
if (params.ip) paramLines.push(` ("ip", "${params.ip}"),`);
|
||||
if (params.depth && params.depth > 1) paramLines.push(` ("depth", "${params.depth}"),`);
|
||||
|
||||
return `import requests
|
||||
|
||||
FLOWFISH_URL = "${API_BASE}"
|
||||
FLOWFISH_API_KEY = "**********" # Get from Settings > API Keys
|
||||
|
||||
resp = requests.get(
|
||||
f"{FLOWFISH_URL}/communications/dependencies/summary",
|
||||
params=[
|
||||
${paramLines.join('\n')}
|
||||
],
|
||||
headers={"X-API-Key": FLOWFISH_API_KEY},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
deps = resp.json()
|
||||
|
||||
# Extract affected git repos from downstream annotations
|
||||
affected_repos = []
|
||||
for category, services in deps.get("downstream", {}).get("by_category", {}).items():
|
||||
for svc in services:
|
||||
repo = svc.get("annotations", {}).get("git-repo")
|
||||
if repo:
|
||||
affected_repos.append({
|
||||
"repo": repo,
|
||||
"service": svc["name"],
|
||||
"namespace": svc["namespace"],
|
||||
"category": category,
|
||||
"critical": svc.get("is_critical", False),
|
||||
})
|
||||
|
||||
print(f"Found {len(affected_repos)} affected repositories")
|
||||
for r in affected_repos:
|
||||
flag = " [CRITICAL]" if r["critical"] else ""
|
||||
print(f" {r['service']} ({r['category']}){flag} -> {r['repo']}")`;
|
||||
}
|
||||
|
||||
export function buildJsSnippet(params: DependencySummaryParams | null): string {
|
||||
const qsStr = buildQueryString(params);
|
||||
if (!qsStr) return '';
|
||||
const API_BASE = getApiBase();
|
||||
return `// Get your API key from Flowfish Settings > API Keys
|
||||
const FLOWFISH_API_KEY = "**********";
|
||||
const FLOWFISH_URL = "${API_BASE}";
|
||||
|
||||
const resp = await fetch(
|
||||
\`\${FLOWFISH_URL}/communications/dependencies/summary?${qsStr}\`,
|
||||
{ headers: { "X-API-Key": FLOWFISH_API_KEY } }
|
||||
);
|
||||
if (!resp.ok) throw new Error(\`HTTP \${resp.status}: \${await resp.text()}\`);
|
||||
const deps = await resp.json();
|
||||
|
||||
// Extract affected repos
|
||||
const affectedRepos = Object.entries(deps.downstream?.by_category ?? {})
|
||||
.flatMap(([category, services]) =>
|
||||
services
|
||||
.filter(svc => svc.annotations?.["git-repo"])
|
||||
.map(svc => ({
|
||||
repo: svc.annotations["git-repo"],
|
||||
service: svc.name,
|
||||
category,
|
||||
critical: svc.is_critical,
|
||||
}))
|
||||
);
|
||||
|
||||
console.log(\`Found \${affectedRepos.length} affected repos\`);`;
|
||||
}
|
||||
|
||||
export function buildPipelineSnippet(
|
||||
params: DependencySummaryParams | null,
|
||||
platform: string,
|
||||
): string {
|
||||
const qsStr = buildQueryString(params);
|
||||
if (!qsStr) return '';
|
||||
const baseUrl = getBaseUrl();
|
||||
|
||||
if (platform === 'azure_devops') {
|
||||
return `# Azure DevOps Pipeline - Flowfish Integration
|
||||
# Set FLOWFISH_API_KEY as a secret variable in Pipeline Settings > Variables
|
||||
variables:
|
||||
FLOWFISH_URL: '${baseUrl}'
|
||||
FLOWFISH_QUERY: '${qsStr}'
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
DEPS=$(curl -sf -H "X-API-Key: $(FLOWFISH_API_KEY)" \\
|
||||
"$(FLOWFISH_URL)/api/v1/communications/dependencies/summary?$(FLOWFISH_QUERY)")
|
||||
echo "$DEPS" > flowfish-deps.json
|
||||
|
||||
CRITICAL=$(echo "$DEPS" | python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
c=d.get('downstream',{}).get('critical_count',0)
|
||||
print(c)
|
||||
")
|
||||
echo "##vso[task.setvariable variable=CRITICAL_DEPS]$CRITICAL"
|
||||
displayName: 'Flowfish: Get Cross-Project Dependencies'
|
||||
env:
|
||||
FLOWFISH_API_KEY: $(FLOWFISH_API_KEY)
|
||||
FLOWFISH_URL: $(FLOWFISH_URL)
|
||||
|
||||
- script: |
|
||||
python ai-agent/analyze.py \\
|
||||
--pr-diff $(System.PullRequest.PullRequestId) \\
|
||||
--deps flowfish-deps.json
|
||||
displayName: 'AI Impact Analysis (Cross-Project)'
|
||||
condition: succeededOrFailed()`;
|
||||
}
|
||||
|
||||
if (platform === 'github_actions') {
|
||||
return `# GitHub Actions - Flowfish Integration
|
||||
# Store your API key in repository secrets as FLOWFISH_API_KEY
|
||||
# Set FLOWFISH_URL in repository variables (Settings > Secrets and variables > Actions)
|
||||
env:
|
||||
FLOWFISH_QUERY: '${qsStr}'
|
||||
|
||||
jobs:
|
||||
flowfish:
|
||||
steps:
|
||||
- name: Get Flowfish Dependencies
|
||||
id: flowfish
|
||||
run: |
|
||||
curl -sf -H "X-API-Key: \${{ secrets.FLOWFISH_API_KEY }}" \\
|
||||
"\${{ vars.FLOWFISH_URL }}/api/v1/communications/dependencies/summary?\${FLOWFISH_QUERY}" \\
|
||||
> flowfish-deps.json
|
||||
|
||||
CRITICAL=$(python3 -c "
|
||||
import json
|
||||
d=json.load(open('flowfish-deps.json'))
|
||||
print(d.get('downstream',{}).get('critical_count',0))
|
||||
")
|
||||
echo "critical_deps=$CRITICAL" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: AI Impact Analysis
|
||||
run: |
|
||||
python ai-agent/analyze.py \\
|
||||
--pr-diff \${{ github.event.pull_request.number }} \\
|
||||
--deps flowfish-deps.json`;
|
||||
}
|
||||
|
||||
if (platform === 'gitlab_ci') {
|
||||
return `# GitLab CI - Flowfish Integration
|
||||
# Store FLOWFISH_API_KEY and FLOWFISH_URL as CI/CD variables
|
||||
variables:
|
||||
FLOWFISH_URL: '${baseUrl}'
|
||||
FLOWFISH_QUERY: '${qsStr}'
|
||||
|
||||
flowfish_dependencies:
|
||||
stage: test
|
||||
script:
|
||||
- |
|
||||
curl -sf -H "X-API-Key: $FLOWFISH_API_KEY" \\
|
||||
"$FLOWFISH_URL/api/v1/communications/dependencies/summary?$FLOWFISH_QUERY" \\
|
||||
> flowfish-deps.json
|
||||
- python ai-agent/analyze.py --deps flowfish-deps.json
|
||||
artifacts:
|
||||
paths:
|
||||
- flowfish-deps.json`;
|
||||
}
|
||||
|
||||
if (platform === 'jenkins') {
|
||||
return `// Jenkins Pipeline - Flowfish Integration
|
||||
// Store API key as a Secret Text credential named 'flowfish-api-key'
|
||||
def FLOWFISH_URL = '${baseUrl}'
|
||||
def FLOWFISH_QUERY = '${qsStr}'
|
||||
|
||||
stage('Flowfish Dependencies') {
|
||||
steps {
|
||||
withCredentials([string(credentialsId: 'flowfish-api-key', variable: 'FLOWFISH_API_KEY')]) {
|
||||
script {
|
||||
def deps = sh(returnStdout: true, script: """
|
||||
curl -sf -H "X-API-Key: \${FLOWFISH_API_KEY}" \\
|
||||
"\${FLOWFISH_URL}/api/v1/communications/dependencies/summary?\${FLOWFISH_QUERY}"
|
||||
""").trim()
|
||||
writeFile file: 'flowfish-deps.json', text: deps
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
return `# Generic CI/CD - Flowfish Integration
|
||||
# Get your API key from Flowfish Settings > API Keys
|
||||
FLOWFISH_API_KEY='**********'
|
||||
FLOWFISH_URL='${baseUrl}'
|
||||
FLOWFISH_QUERY='${qsStr}'
|
||||
|
||||
curl -sf -H "X-API-Key: $FLOWFISH_API_KEY" \\
|
||||
"$FLOWFISH_URL/api/v1/communications/dependencies/summary?$FLOWFISH_QUERY" \\
|
||||
> flowfish-deps.json`;
|
||||
}
|
||||
|
||||
export function buildBlastRadiusCurlSnippet(
|
||||
namespace?: string,
|
||||
ownerName?: string,
|
||||
): string {
|
||||
const baseUrl = getBaseUrl();
|
||||
const target = ownerName || 'your-service-name';
|
||||
const ns = namespace || 'default';
|
||||
|
||||
return `# Blast Radius - Pre-deployment Impact Assessment
|
||||
# Returns risk score, affected services, and recommendations
|
||||
FLOWFISH_API_KEY='**********' # Get from Settings > API Keys
|
||||
|
||||
curl -s -X POST "${baseUrl}/api/v1/blast-radius/assess" \\
|
||||
-H "X-API-Key: $FLOWFISH_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"cluster_id": 1,
|
||||
"change": {
|
||||
"type": "image_update",
|
||||
"target": "${target}",
|
||||
"namespace": "${ns}",
|
||||
"triggered_by": "ci-pipeline",
|
||||
"pipeline": "main-deploy"
|
||||
}
|
||||
}'
|
||||
|
||||
# Response includes:
|
||||
# risk_score (0-100), risk_level (low/medium/high/critical),
|
||||
# blast_radius.total_affected, recommendation, suggested_actions[]
|
||||
# advisory_only: true (Flowfish never blocks deployments)`;
|
||||
}
|
||||
|
||||
export function buildBlastRadiusPipelineSnippet(
|
||||
platform: string,
|
||||
namespace?: string,
|
||||
ownerName?: string,
|
||||
): string {
|
||||
const baseUrl = getBaseUrl();
|
||||
const target = ownerName || 'your-service-name';
|
||||
const ns = namespace || 'default';
|
||||
|
||||
if (platform === 'azure_devops') {
|
||||
return `# Azure DevOps - Flowfish Blast Radius Check
|
||||
# Set FLOWFISH_API_KEY as a secret variable
|
||||
variables:
|
||||
FLOWFISH_URL: '${baseUrl}'
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
RESPONSE=$(curl -s -X POST "$(FLOWFISH_URL)/api/v1/blast-radius/assess" \\
|
||||
-H "X-API-Key: $(FLOWFISH_API_KEY)" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"cluster_id": $(CLUSTER_ID),
|
||||
"change": {
|
||||
"type": "image_update",
|
||||
"target": "${target}",
|
||||
"namespace": "${ns}",
|
||||
"triggered_by": "$(Build.RequestedFor)",
|
||||
"pipeline": "$(Build.DefinitionName)"
|
||||
}
|
||||
}')
|
||||
|
||||
RISK_SCORE=$(echo "$RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('risk_score',0))")
|
||||
echo "##vso[task.setvariable variable=RISK_SCORE]$RISK_SCORE"
|
||||
echo "Risk Score: $RISK_SCORE/100"
|
||||
displayName: 'Flowfish: Blast Radius Check'
|
||||
continueOnError: true
|
||||
env:
|
||||
FLOWFISH_API_KEY: $(FLOWFISH_API_KEY)`;
|
||||
}
|
||||
|
||||
if (platform === 'github_actions') {
|
||||
return `# GitHub Actions - Flowfish Blast Radius Check
|
||||
- name: Flowfish Blast Radius Check
|
||||
id: blast-radius
|
||||
continue-on-error: true
|
||||
run: |
|
||||
RESPONSE=$(curl -s -X POST \\
|
||||
"\${{ secrets.FLOWFISH_URL }}/api/v1/blast-radius/assess" \\
|
||||
-H "X-API-Key: \${{ secrets.FLOWFISH_API_KEY }}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"cluster_id": \${{ vars.CLUSTER_ID }},
|
||||
"change": {
|
||||
"type": "image_update",
|
||||
"target": "${target}",
|
||||
"namespace": "${ns}",
|
||||
"triggered_by": "\${{ github.actor }}",
|
||||
"pipeline": "\${{ github.workflow }}"
|
||||
}
|
||||
}')
|
||||
|
||||
RISK_SCORE=$(echo "$RESPONSE" | jq -r '.risk_score // 0')
|
||||
echo "risk_score=$RISK_SCORE" >> $GITHUB_OUTPUT
|
||||
echo "Risk Score: $RISK_SCORE/100"`;
|
||||
}
|
||||
|
||||
if (platform === 'gitlab_ci') {
|
||||
return `# GitLab CI - Flowfish Blast Radius Check
|
||||
flowfish_blast_radius:
|
||||
stage: test
|
||||
allow_failure: true
|
||||
script:
|
||||
- |
|
||||
RESPONSE=$(curl -s -X POST "$FLOWFISH_URL/api/v1/blast-radius/assess" \\
|
||||
-H "X-API-Key: $FLOWFISH_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"cluster_id": '$CLUSTER_ID',
|
||||
"change": {
|
||||
"type": "image_update",
|
||||
"target": "${target}",
|
||||
"namespace": "${ns}",
|
||||
"triggered_by": "'$GITLAB_USER_LOGIN'",
|
||||
"pipeline": "'$CI_PIPELINE_NAME'"
|
||||
}
|
||||
}')
|
||||
echo "Risk Score: $(echo $RESPONSE | jq -r '.risk_score')/100"`;
|
||||
}
|
||||
|
||||
if (platform === 'jenkins') {
|
||||
return `// Jenkins - Flowfish Blast Radius Check
|
||||
stage('Blast Radius Check') {
|
||||
steps {
|
||||
script {
|
||||
def response = httpRequest(
|
||||
url: "\${FLOWFISH_URL}/api/v1/blast-radius/assess",
|
||||
httpMode: 'POST',
|
||||
contentType: 'APPLICATION_JSON',
|
||||
customHeaders: [[name: 'X-API-Key', value: "\${FLOWFISH_API_KEY}"]],
|
||||
requestBody: """{
|
||||
"cluster_id": \${CLUSTER_ID},
|
||||
"change": {
|
||||
"type": "image_update",
|
||||
"target": "${target}",
|
||||
"namespace": "${ns}",
|
||||
"triggered_by": "\${BUILD_USER}",
|
||||
"pipeline": "\${JOB_NAME}"
|
||||
}
|
||||
}""",
|
||||
validResponseCodes: '200:500'
|
||||
)
|
||||
if (response.status == 200) {
|
||||
def result = readJSON(text: response.content)
|
||||
echo "Risk Score: \${result.risk_score}/100 (\${result.risk_level})"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
return `# Generic CI/CD - Flowfish Blast Radius Check
|
||||
FLOWFISH_API_KEY='**********'
|
||||
FLOWFISH_URL='${baseUrl}'
|
||||
|
||||
curl -s -X POST "$FLOWFISH_URL/api/v1/blast-radius/assess" \\
|
||||
-H "X-API-Key: $FLOWFISH_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"cluster_id": 1,
|
||||
"change": {
|
||||
"type": "image_update",
|
||||
"target": "${target}",
|
||||
"namespace": "${ns}",
|
||||
"triggered_by": "ci-pipeline",
|
||||
"pipeline": "main-deploy"
|
||||
}
|
||||
}'`;
|
||||
}
|
||||
|
||||
export const PIPELINE_PLATFORMS = [
|
||||
{ value: 'azure_devops', label: 'Azure DevOps' },
|
||||
{ value: 'github_actions', label: 'GitHub Actions' },
|
||||
{ value: 'gitlab_ci', label: 'GitLab CI' },
|
||||
{ value: 'jenkins', label: 'Jenkins' },
|
||||
{ value: 'other', label: 'Other (Generic)' },
|
||||
];
|
||||
|
||||
export const ID_METHODS = [
|
||||
{ value: 'annotation', label: 'Annotation (e.g. git-repo URL)' },
|
||||
{ value: 'label', label: 'Label (e.g. app name)' },
|
||||
{ value: 'namespace_deployment', label: 'Namespace + Deployment' },
|
||||
{ value: 'pod_name', label: 'Pod Name' },
|
||||
{ value: 'advanced', label: 'Advanced (any combination)' },
|
||||
];
|
||||
Reference in New Issue
Block a user