Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEV-2617: Improved AtmosWorkflows #703

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/';
113 changes: 45 additions & 68 deletions src/components/AtmosWorkflow/index.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,73 @@
// 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}
</h4>
) : (
<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[];
}
108 changes: 108 additions & 0 deletions src/components/AtmosWorkflow/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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[] = [];

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

if (command.trim().startsWith('echo')) {
const titleContent = command
.replace(/^echo\s+/, '')
.replace(/^['"]|['"]$/g, '')
.trim();
steps.push({
type: 'title',
content: titleContent,
});
} else if (command.startsWith('workflow')) {
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 (step.type === 'shell') {
const stepName = step.name || 'script';
const shebang = `#!/bin/bash\n`;
const titleComment = `# Run the ${stepName} Script\n`;
const commandWithTitle = `${shebang}${titleComment}${command}`;
steps.push({
type: 'command',
content: commandWithTitle,
});
} else {
let atmosCommand = `atmos ${command}`;
if (stack) {
atmosCommand += ` -s ${stack}`;
}
steps.push({
type: 'command',
content: atmosCommand,
});
}
}

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

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