Skip to content

Commit

Permalink
DEV-2617: Improved AtmosWorkflows (#703)
Browse files Browse the repository at this point in the history
  • Loading branch information
milldr authored Jan 10, 2025
1 parent afe6cc9 commit 49e5695
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 69 deletions.
2 changes: 1 addition & 1 deletion docs/layers/gitops/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ import AtmosWorkflow from '@site/src/components/AtmosWorkflow';

Deploy three components, `gitops/s3-bucket`, `gitops/dynamodb`, and `gitops` with the following workflow:

<AtmosWorkflow workflow="deploy" fileName="gitops" />
<AtmosWorkflow workflow="deploy/gitops" fileName="gitops" />

And that's it!
</Step>
Expand Down
4 changes: 4 additions & 0 deletions src/components/AtmosWorkflow/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// constants.ts

export const CLOUDPOSSE_DOCS_URL = 'https://raw.githubusercontent.com/cloudposse/docs/master/';
export const WORKFLOWS_DIRECTORY_PATH = 'examples/snippets/stacks/workflows/';
118 changes: 50 additions & 68 deletions src/components/AtmosWorkflow/index.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,78 @@
// index.tsx

import React, { useEffect, useState } from 'react';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import Note from '@site/src/components/Note';
import Steps from '@site/src/components/Steps';
import TabItem from '@theme/TabItem';
import Tabs from '@theme/Tabs';

import * as yaml from 'js-yaml';

// Define constants for the base URL and workflows directory path
const CLOUDPOSSE_DOCS_URL = 'https://raw.githubusercontent.com/cloudposse/docs/master/';
const WORKFLOWS_DIRECTORY_PATH = 'examples/snippets/stacks/workflows/';

async function GetAtmosTerraformCommands(workflow: string, fileName: string, stack?: string): Promise<string[] | undefined> {
try {
// Construct the full URL to the workflow YAML file
const url = `${CLOUDPOSSE_DOCS_URL}${WORKFLOWS_DIRECTORY_PATH}${fileName}.yaml`;

// Fetch the workflow file from the constructed URL
const response = await fetch(url);
if (!response.ok) {
console.error('Failed to fetch the file:', response.statusText);
console.error('Workflow URL:', url);
return undefined;
}
const fileContent = await response.text();

// Parse the YAML content
const workflows = yaml.load(fileContent) as any;

// Find the specified workflow in the parsed YAML
if (workflows && workflows.workflows && workflows.workflows[workflow]) {
const workflowDetails = workflows.workflows[workflow];

// Extract the commands under that workflow
const commands = workflowDetails.steps.map((step: any) => {
let command = step.command;
// TODO handle nested Atmos Workflows
// For example: https://raw.githubusercontent.com/cloudposse/docs/master/examples/snippets/stacks/workflows/identity.yaml
if (!step.type) {
command = `atmos ${command}`;
if (stack) {
command += ` -s ${stack}`;
}
}
return command;
});

return commands;
}
import { GetAtmosTerraformCommands } from './utils';
import { WorkflowStep, WorkflowData } from './types';
import { WORKFLOWS_DIRECTORY_PATH } from './constants';

// Return undefined if the workflow is not found
return undefined;
} catch (error) {
console.error('Error fetching or parsing the file:', error);
return undefined;
}
interface AtmosWorkflowProps {
workflow: string;
stack?: string;
fileName: string;
}

export default function AtmosWorkflow({ workflow, stack = "", fileName }) {
const [commands, setCommands] = useState<string[]>([]);
export default function AtmosWorkflow({ workflow, stack = '', fileName }: AtmosWorkflowProps) {
const [workflowData, setWorkflowData] = useState<WorkflowData | null>(null);
const fullFilePath = `${WORKFLOWS_DIRECTORY_PATH}${fileName}.yaml`;

useEffect(() => {
GetAtmosTerraformCommands(workflow, fileName, stack).then((cmds) => {
if (Array.isArray(cmds)) {
setCommands(cmds);
GetAtmosTerraformCommands(workflow, fileName, stack).then((data) => {
if (data) {
setWorkflowData(data);
} else {
setCommands([]); // Default to an empty array if cmds is undefined or not an array
setWorkflowData(null);
}
});
}, [workflow, fileName, stack]);

return (
<Tabs queryString="workflows">
<TabItem value="commands" label="Commands">
These are the commands included in the <code>{workflow}</code> workflow in the <code>{fullFilePath}</code> file:
<Note title={workflow}>
These are the commands included in the <code>{workflow}</code> workflow in the{' '}
<code>{fullFilePath}</code> file:
</Note>
{workflowData?.description && (
<p className=".workflow-title">
{workflowData.description}
</p>
)}
<Steps>
<ul>
{commands.length > 0 ? commands.map((cmd, index) => (
<li key={index}>
<CodeBlock language="bash">
{cmd}
</CodeBlock>
</li>
)) : 'No commands found'}
{workflowData?.steps.length ? (
workflowData.steps.map((step, index) => (
<li key={index}>
{step.type === 'title' ? (
<>
<h4 className=".workflow-title">
{step.content.split('\n\n')[0]}
</h4>
<CodeBlock language="bash">
{step.content.split('\n\n')[1]}
</CodeBlock>
</>
) : (
<CodeBlock language="bash">{step.content}</CodeBlock>
)}
</li>
))
) : (
'No commands found'
)}
</ul>
</Steps>
Too many commands? Consider using the Atmos workflow! 🚀
<p>Too many commands? Consider using the Atmos workflow! 🚀</p>
</TabItem>
<TabItem value="atmos" label="Atmos Workflow">
Run the following from your Geodesic shell using the Atmos workflow:
<p>Run the following from your Geodesic shell using the Atmos workflow:</p>
<CodeBlock language="bash">
atmos workflow {workflow} -f {fileName} {stack && `-s ${stack}`}
{`atmos workflow ${workflow} -f ${fileName} ${stack ? `-s ${stack}` : ''}`}
</CodeBlock>
</TabItem>
</Tabs>
Expand Down
6 changes: 6 additions & 0 deletions src/components/AtmosWorkflow/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* styles.css */

.workflow-title {
font-size: 1.25em;
color: #2c3e50;
}
11 changes: 11 additions & 0 deletions src/components/AtmosWorkflow/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// types.ts

export interface WorkflowStep {
type: 'command' | 'title';
content: string;
}

export interface WorkflowData {
description?: string;
steps: WorkflowStep[];
}
157 changes: 157 additions & 0 deletions src/components/AtmosWorkflow/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// utils.ts

import * as yaml from 'js-yaml';
import { WorkflowStep, WorkflowData } from './types';
import { CLOUDPOSSE_DOCS_URL, WORKFLOWS_DIRECTORY_PATH } from './constants';

export async function GetAtmosTerraformCommands(
workflow: string,
fileName: string,
stack?: string,
visitedWorkflows = new Set<string>()
): Promise<WorkflowData | undefined> {
try {
const url = `${CLOUDPOSSE_DOCS_URL}${WORKFLOWS_DIRECTORY_PATH}${fileName}.yaml`;

const response = await fetch(url);
if (!response.ok) {
console.error('Failed to fetch the file:', response.statusText);
console.error('Workflow URL:', url);
return undefined;
}
const fileContent = await response.text();

const workflows = yaml.load(fileContent) as any;

if (workflows && workflows.workflows && workflows.workflows[workflow]) {
const workflowDetails = workflows.workflows[workflow];

const workflowKey = `${fileName}:${workflow}`;
if (visitedWorkflows.has(workflowKey)) {
console.warn(
`Already visited workflow ${workflow} in file ${fileName}, skipping to prevent infinite loop.`
);
return { description: workflowDetails.description, steps: [] };
}
visitedWorkflows.add(workflowKey);

let steps: WorkflowStep[] = [];
let currentGroupCommands: string[] = [];
let currentTitle: string | null = null;

const addGroupToSteps = () => {
if (currentGroupCommands.length > 0) {
if (currentTitle) {
steps.push({
type: 'title',
content: `${currentTitle}\n\n${currentGroupCommands.join('\n')}`
});
} else {
steps.push({
type: 'command',
content: currentGroupCommands.join('\n')
});
}
currentGroupCommands = [];
currentTitle = null;
}
};

// Group all vendor pull commands together
const isVendorWorkflow = workflowDetails.steps.every(step =>
step.command.startsWith('vendor pull')
);

for (const step of workflowDetails.steps) {
let command = step.command;

if (isVendorWorkflow) {
// Add all vendor commands to a single group
let atmosCommand = `atmos ${command}`;
if (stack) {
atmosCommand += ` -s ${stack}`;
}
currentGroupCommands.push(atmosCommand);
} else if (command.trim().startsWith('echo') && step.type === 'shell') {
// When we find an echo, add previous group and start new group
addGroupToSteps();
currentTitle = command.replace(/^echo\s+['"](.+)['"]$/, '$1');
} else if (command.startsWith('workflow')) {
// For nested workflows, add current group first
addGroupToSteps();

const commandParts = command.split(' ');
const nestedWorkflowIndex = commandParts.findIndex((part) => part === 'workflow') + 1;
const nestedWorkflow = commandParts[nestedWorkflowIndex];

let nestedFileName = fileName;
const fileFlagIndex = commandParts.findIndex((part) => part === '-f' || part === '--file');
if (fileFlagIndex !== -1) {
nestedFileName = commandParts[fileFlagIndex + 1];
}

let nestedStack = stack;
const stackFlagIndex = commandParts.findIndex((part) => part === '-s' || part === '--stack');
if (stackFlagIndex !== -1) {
nestedStack = commandParts[stackFlagIndex + 1];
}

const nestedData = await GetAtmosTerraformCommands(
nestedWorkflow,
nestedFileName,
nestedStack,
visitedWorkflows
);

if (nestedData && nestedData.steps) {
steps = steps.concat(nestedData.steps);
}
} else {
if (currentTitle) {
// We're in an echo group
if (step.type === 'shell') {
const shebang = `#!/bin/bash\n`;
const titleComment = `# Run the ${step.name || 'script'} Script\n`;
currentGroupCommands.push(`${shebang}${titleComment}${command}`);
} else {
let atmosCommand = `atmos ${command}`;
if (stack) {
atmosCommand += ` -s ${stack}`;
}
currentGroupCommands.push(atmosCommand);
}
} else {
// Individual step
if (step.type === 'shell') {
const shebang = `#!/bin/bash\n`;
const titleComment = `# Run the ${step.name || 'script'} Script\n`;
steps.push({
type: 'command',
content: `${shebang}${titleComment}${command}`,
});
} else {
let atmosCommand = `atmos ${command}`;
if (stack) {
atmosCommand += ` -s ${stack}`;
}
steps.push({
type: 'command',
content: atmosCommand,
});
}
}
}
}

// Add any remaining grouped commands
addGroupToSteps();

return { description: workflowDetails.description, steps };
}

return undefined;
} catch (error) {
console.error('Error fetching or parsing the file:', error);
return undefined;
}
}

0 comments on commit 49e5695

Please sign in to comment.