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

export command #54

Open
wants to merge 76 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
e84ef9b
export command
daveads Dec 17, 2024
087cc4a
format export.test.tsx with Prettier
daveads Dec 17, 2024
3d80656
refactored
daveads Dec 17, 2024
9ab222d
updates
daveads Dec 17, 2024
dcfeb1e
tests
daveads Dec 18, 2024
8239bff
format ResourceGenerator.test.tsxr
daveads Dec 18, 2024
117d7ec
Merge branch 'main' into export-command
daveads Dec 29, 2024
5eeae2e
lint
daveads Dec 29, 2024
7709b39
run build error
daveads Dec 29, 2024
9d2b9c0
format
daveads Dec 29, 2024
ca44af2
any >>
daveads Dec 29, 2024
de8b8b9
..
daveads Dec 29, 2024
2478730
test
daveads Dec 29, 2024
cc061d9
hooks, components
daveads Dec 30, 2024
19ef6c2
(refactor) move HCL generation to Handlebars template
daveads Dec 31, 2024
c346833
move HCL output to render function
daveads Dec 31, 2024
3b1b3b3
improve error rendering with concise conditional logic
daveads Dec 31, 2024
da5a34d
review
daveads Dec 31, 2024
ff22083
added missing resource and also depends_on
daveads Jan 4, 2025
8219d7d
prettier
daveads Jan 4, 2025
bce2cbb
test
daveads Jan 5, 2025
e1231c4
typescript errors
daveads Jan 5, 2025
508c3a3
lint..
daveads Jan 5, 2025
39d159b
lint..
daveads Jan 5, 2025
b2a591c
format
daveads Jan 5, 2025
ebd8172
actiondata
daveads Jan 5, 2025
8b466ff
test
daveads Jan 5, 2025
a5dc8fe
..
daveads Jan 5, 2025
009c4c6
test
daveads Jan 5, 2025
f403813
Merge branch 'main' into export-command
daveads Jan 5, 2025
dda9be6
..
daveads Jan 5, 2025
307dfea
include .hcl files in build output
daveads Jan 7, 2025
e8f9eb3
npm build and npm dev
daveads Jan 10, 2025
e7ddd4c
test
daveads Jan 10, 2025
187ebb0
resource set
daveads Jan 20, 2025
7aabb4b
fixing exported output
daveads Jan 21, 2025
66d60e7
definition
daveads Jan 28, 2025
bcc22cb
debug
daveads Feb 4, 2025
f0a4c4b
role
daveads Feb 6, 2025
8e6cf90
clean up
daveads Feb 6, 2025
e3e0030
build
daveads Feb 7, 2025
7873495
prettier
daveads Feb 7, 2025
bbe5eb7
prettier
daveads Feb 7, 2025
b9cfb90
prettier
daveads Feb 7, 2025
7c625a7
test
daveads Feb 8, 2025
8060b54
Merge branch 'main' into export-command
gemanor Feb 10, 2025
870214f
fix
daveads Feb 11, 2025
3ced990
fix user-attribute, resource generator, excluded default roles in rol…
daveads Feb 23, 2025
332bafa
fix output
daveads Feb 25, 2025
b06de7f
test works..
daveads Feb 28, 2025
cb46671
action
daveads Feb 28, 2025
a887622
Merge branch 'main' into export-command
daveads Feb 28, 2025
767b1eb
Merge branch 'export-command' into action
daveads Feb 28, 2025
375cfe8
..
daveads Feb 28, 2025
31ba2c0
prettier
daveads Feb 28, 2025
4e72b85
lint
daveads Feb 28, 2025
f43889e
test
daveads Feb 28, 2025
65554df
Merge branch 'export-command' into action
daveads Feb 28, 2025
3a034e1
test...
daveads Mar 3, 2025
e5a026b
prettier
daveads Mar 4, 2025
0b11a8d
..
daveads Mar 4, 2025
fc80788
fix
daveads Mar 4, 2025
3ac73bb
fix
daveads Mar 4, 2025
4c1c3de
empty des
daveads Mar 4, 2025
4a65034
updates...
daveads Mar 4, 2025
e983afc
updates and fix
daveads Mar 5, 2025
8b17a2a
duplicates
daveads Mar 5, 2025
697f4d9
fix
daveads Mar 5, 2025
b37b076
current...
daveads Mar 9, 2025
ac0c5c2
build lint issue...
daveads Mar 9, 2025
f12c434
fix build issue
daveads Mar 9, 2025
86a6c3a
tests...
daveads Mar 9, 2025
cd01ac7
Merge branch 'main' into export-command
daveads Mar 9, 2025
050f48c
builtin attribute
daveads Mar 9, 2025
1705ea3
fix
daveads Mar 10, 2025
3ffba6d
test..
daveads Mar 10, 2025
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
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default [
RequestInit: 'readonly',
fetch: 'readonly',
process: 'readonly',
console: true,
},
},
},
Expand Down
114 changes: 114 additions & 0 deletions source/commands/env/export/components/ExportContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React from 'react';
import { useApiKeyApi } from '../../../../hooks/useApiKeyApi.js';
import { useAuth } from '../../../../components/AuthProvider.js';
import { ExportOptions } from '../types.js';
import { ExportStatus } from './ExportStatus.js';
import { useExport } from './hooks/useExport.js';
import fs from 'node:fs/promises';

export const ExportContent: React.FC<{ options: ExportOptions }> = ({
options: { key: apiKey, file },
}) => {
const { validateApiKeyScope } = useApiKeyApi();
const { authToken } = useAuth();
const key = apiKey || authToken;
const { state, setState, exportConfig } = useExport(key);

React.useEffect(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the convention of importing the function and use them without the React.

import React, { useEffect } from 'react';

...

useEffect(() => {

let isSubscribed = true;

const runExport = async () => {
if (!key) {
setState({
status: '',
error: 'No API key provided. Please provide a key or login first.',
isComplete: true,
warnings: [],
});
return;
}

try {
setState(prev => ({ ...prev, status: 'Validating API key...' }));
const {
valid,
error: scopeError,
scope,
} = await validateApiKeyScope(key, 'environment');

if (!valid || scopeError || !scope) {
setState({
status: '',
error: `Invalid API key: ${scopeError || 'No scope found'}`,
isComplete: true,
warnings: [],
});
return;
}

// Normalize the environment_id and project_id to match ExportScope
const normalizedScope = {
...scope,
environment_id: scope.environment_id || undefined,
project_id: scope.project_id || undefined,
};

if (!isSubscribed) return;

setState(prev => ({
...prev,
status: 'Initializing export...',
}));

const { hcl, warnings } = await exportConfig(normalizedScope);

if (!isSubscribed) return;

if (file) {
setState(prev => ({ ...prev, status: 'Saving to file...' }));
try {
await fs.writeFile(file, hcl);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
setState({
status: '',
error: `Failed to export configuration: ${errorMessage}`,
isComplete: true,
warnings: [],
});
return;
}
} else {
console.log(hcl);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be printed as part of render function, using the console.log here can be confusing and lead to unmaintainable code

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

render function, means to render it as part of the command/component return section

}

if (!isSubscribed) return;

setState({
status: '',
error: null,
isComplete: true,
warnings,
});
} catch (err) {
if (!isSubscribed) return;
const errorMsg = err instanceof Error ? err.message : String(err);
setState({
status: '',
error: `Failed to export configuration: ${errorMsg}`,
isComplete: true,
warnings: [],
});
}
};

runExport();

return () => {
isSubscribed = false;
};
}, [key, file, validateApiKeyScope]);

Check warning on line 111 in source/commands/env/export/components/ExportContent.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

React Hook React.useEffect has missing dependencies: 'exportConfig' and 'setState'. Either include them or remove the dependency array

Check warning on line 111 in source/commands/env/export/components/ExportContent.tsx

View workflow job for this annotation

GitHub Actions / build (20.x)

React Hook React.useEffect has missing dependencies: 'exportConfig' and 'setState'. Either include them or remove the dependency array

Check warning on line 111 in source/commands/env/export/components/ExportContent.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

React Hook React.useEffect has missing dependencies: 'exportConfig' and 'setState'. Either include them or remove the dependency array

return <ExportStatus state={state} file={file} />;
};
57 changes: 57 additions & 0 deletions source/commands/env/export/components/ExportStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { Text } from 'ink';
import Spinner from 'ink-spinner';
import { ExportState } from '../types.js';

interface ExportStatusProps {
state: ExportState;
file?: string;
}

export const ExportStatus: React.FC<ExportStatusProps> = ({ state, file }) => {
if (state.error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace the if (state.error) { convention with {state.error && convention, to make it more readable

return (
<>
<Text color="red">Error: {state.error}</Text>
{state.warnings.length > 0 && (
<>
<Text color="yellow">Warnings:</Text>
{state.warnings.map((warning, i) => (
<Text key={i} color="yellow">
- {warning}
</Text>
))}
</>
)}
</>
);
}

if (!state.isComplete) {
return (
<>
<Text>
<Spinner type="dots" />{' '}
{state.status || 'Exporting environment configuration...'}
</Text>
</>
);
}

return (
<>
<Text color="green">Export completed successfully!</Text>
{file && <Text>HCL content has been saved to: {file}</Text>}
{state.warnings.length > 0 && (
<>
<Text color="yellow">Warnings during export:</Text>
{state.warnings.map((warning, i) => (
<Text key={i} color="yellow">
- {warning}
</Text>
))}
</>
)}
</>
);
};
13 changes: 13 additions & 0 deletions source/commands/env/export/components/hooks/PermitSDK.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Permit } from 'permitio';
import React from 'react';

export const PermitSDK = (token: string) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the hook naming convention usePermitSDK

return React.useMemo(
() =>
new Permit({
token,
pdp: 'http://localhost:7766',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable should be configurable from the function arguments to allow configurable pdp URL for further usages

}),
[token],
);
};
70 changes: 70 additions & 0 deletions source/commands/env/export/components/hooks/useExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useState } from 'react';
import { PermitSDK } from './PermitSDK.js';
import { ExportState } from '../../types.js';
import { createWarningCollector, generateProviderBlock } from '../../utils.js';
import { ResourceGenerator } from '../../generators/ResourceGenerator.js';
import { RoleGenerator } from '../../generators/RoleGenerator.js';
import { UserAttributesGenerator } from '../../generators/UserAttributesGenerator.js';
import { RelationGenerator } from '../../generators/RelationGenerator.js';
import { ConditionSetGenerator } from '../../generators/ConditionSetGenerator.js';

// Define a type for the `scope` parameter
interface ExportScope {
environment_id?: string;
project_id?: string;
organization_id?: string;
}

export const useExport = (apiKey: string) => {
const [state, setState] = useState<ExportState>({
status: '',
isComplete: false,
error: null,
warnings: [],
});

const permit = PermitSDK(apiKey);

const exportConfig = async (scope: ExportScope) => {
try {
const warningCollector = createWarningCollector();

let hcl = `# Generated by Permit CLI
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move all the .hcl code to static .hcl files in a dedicated folder and import them to the code where needed.
It will:

  1. Made the code more readable and keep the indentation
  2. Ensure reusable of code snippets

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is up to you to decide on the import way of this text and use the templates in it

# Environment: ${scope?.environment_id || 'unknown'}
# Project: ${scope?.project_id || 'unknown'}
# Organization: ${scope?.organization_id || 'unknown'}
${generateProviderBlock(apiKey)}`;

const generators = [
new ResourceGenerator(permit, warningCollector),
new RoleGenerator(permit, warningCollector),
new UserAttributesGenerator(permit, warningCollector),
new RelationGenerator(permit, warningCollector),
new ConditionSetGenerator(permit, warningCollector),
];

for (const generator of generators) {
setState(prev => ({
...prev,
status: `Exporting ${generator.name}...`,
}));

const generatedHCL = await generator.generateHCL();
if (generatedHCL) {
hcl += generatedHCL;
}
}

return { hcl, warnings: warningCollector.getWarnings() };
} catch (error) {
console.error('Export error:', error);
throw error;
}
};

return {
state,
setState,
exportConfig,
};
};
62 changes: 62 additions & 0 deletions source/commands/env/export/generators/ConditionSetGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Permit } from 'permitio';
import { HCLGenerator, WarningCollector } from '../types.js';
import { createSafeId } from '../utils.js';

export class ConditionSetGenerator implements HCLGenerator {
name = 'condition sets';

constructor(
private permit: Permit,
private warningCollector: WarningCollector,
) {}

async generateHCL(): Promise<string> {
try {
const conditionSets = await this.permit.api.conditionSets.list();
if (
!conditionSets ||
!Array.isArray(conditionSets) ||
conditionSets.length === 0
) {
return '';
}

let hcl = '\n# Condition Sets\n';

for (const set of conditionSets) {
try {
const isResourceSet = set.type === 'resourceset';
const resourceType = isResourceSet ? 'resource_set' : 'user_set';

// Handle conditions - ensure they are properly stringified
const conditions =
typeof set.conditions === 'string'
? set.conditions
: JSON.stringify(set.conditions || '');

hcl += `resource "permitio_${resourceType}" "${createSafeId(set.key)}" {
key = "${set.key}"
name = "${set.name}"${
set.description ? `\n description = "${set.description}"` : ''
}
conditions = ${conditions}${
set.resource ? `\n resource = "${set.resource}"` : ''
}
}\n`;
} catch (setError) {
this.warningCollector.addWarning(
`Failed to export condition set ${set.key}: ${setError}`,
);
continue;
}
}

return hcl;
} catch (error) {
this.warningCollector.addWarning(
`Failed to export condition sets: ${error}`,
);
return '';
}
}
}
Loading
Loading