diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx index 5e6af3b36..1ed7a1c0c 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx @@ -27,6 +27,7 @@ import { useOrganizationId } from "/hooks/useOrganizationId"; import type { FrameworkGraphNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphNodeQuery.graphql"; import type { FrameworkDetailPageFragment$key } from "./__generated__/FrameworkDetailPageFragment.graphql"; import type { FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation } from "./__generated__/FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation.graphql"; +import type { FrameworkDetailPageExportFrameworkMutation } from "./__generated__/FrameworkDetailPageExportFrameworkMutation.graphql"; import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog"; import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; @@ -66,6 +67,18 @@ const generateFrameworkStateOfApplicabilityMutation = graphql` } `; +const exportFrameworkMutation = graphql` + mutation FrameworkDetailPageExportFrameworkMutation( + $frameworkId: ID! + ) { + exportFramework( + input: { frameworkId: $frameworkId } + ) { + exportJobId + } + } +`; + type Props = { queryRef: PreloadedQuery; }; @@ -100,6 +113,15 @@ export default function FrameworkDetailPage(props: Props) { } ); + const [exportFramework] = + useMutationWithToasts( + exportFrameworkMutation, + { + errorMessage: "Failed to export framework", + successMessage: "Framework export started successfully", + } + ); + usePageTitle(`${framework.name} | ${selectedControl?.sectionTitle}`); const onDelete = () => { deleteFramework({ @@ -156,6 +178,16 @@ export default function FrameworkDetailPage(props: Props) { > {__("Download SOA")} + { + exportFramework({ + variables: { frameworkId: framework.id }, + }); + }} + > + {__("Export Framework")} + {__("Delete")} diff --git a/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageExportFrameworkMutation.graphql.ts b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageExportFrameworkMutation.graphql.ts new file mode 100644 index 000000000..1399513b2 --- /dev/null +++ b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageExportFrameworkMutation.graphql.ts @@ -0,0 +1,95 @@ +/** + * @generated SignedSource<<05766c7969f5e78f91ca5121d2c30db8>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type FrameworkDetailPageExportFrameworkMutation$variables = { + frameworkId: string; +}; +export type FrameworkDetailPageExportFrameworkMutation$data = { + readonly exportFramework: { + readonly exportJobId: string; + }; +}; +export type FrameworkDetailPageExportFrameworkMutation = { + response: FrameworkDetailPageExportFrameworkMutation$data; + variables: FrameworkDetailPageExportFrameworkMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "frameworkId" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "fields": [ + { + "kind": "Variable", + "name": "frameworkId", + "variableName": "frameworkId" + } + ], + "kind": "ObjectValue", + "name": "input" + } + ], + "concreteType": "ExportFrameworkPayload", + "kind": "LinkedField", + "name": "exportFramework", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "exportJobId", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FrameworkDetailPageExportFrameworkMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FrameworkDetailPageExportFrameworkMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "d6fdd2bb2dacfc88248ccfc6f671eb4a", + "id": null, + "metadata": {}, + "name": "FrameworkDetailPageExportFrameworkMutation", + "operationKind": "mutation", + "text": "mutation FrameworkDetailPageExportFrameworkMutation(\n $frameworkId: ID!\n) {\n exportFramework(input: {frameworkId: $frameworkId}) {\n exportJobId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "3866a75fb0aeaaa6cdf97934508116c1"; + +export default node; diff --git a/pkg/coredata/framework_export.go b/pkg/coredata/framework_export.go index c09acd288..cefdc4ea0 100644 --- a/pkg/coredata/framework_export.go +++ b/pkg/coredata/framework_export.go @@ -65,15 +65,13 @@ INSERT INTO framework_exports ( tenant_id, framework_id, status, - created_at, - expires_at + created_at ) VALUES ( @id, @tenant_id, @framework_id, @status, - @created_at, - @expires_at + @created_at )` args := pgx.StrictNamedArgs{ @@ -130,6 +128,7 @@ SELECT id, framework_id, status, + file_id, created_at, started_at, completed_at diff --git a/pkg/coredata/measure.go b/pkg/coredata/measure.go index 84e79cd69..25bbd3975 100644 --- a/pkg/coredata/measure.go +++ b/pkg/coredata/measure.go @@ -249,8 +249,6 @@ WHERE %s ` q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment()) - fmt.Printf("\n%s\n", q) - args := pgx.NamedArgs{"control_id": controlID} maps.Copy(args, scope.SQLArguments()) maps.Copy(args, filter.SQLArguments()) diff --git a/pkg/coredata/migrations/20250828T091522Z.sql b/pkg/coredata/migrations/20250828T091522Z.sql index eb36773fb..bb4340a44 100644 --- a/pkg/coredata/migrations/20250828T091522Z.sql +++ b/pkg/coredata/migrations/20250828T091522Z.sql @@ -1,6 +1,6 @@ CREATE TYPE framework_export_status AS ENUM ( 'pending', - 'processing', + 'processing', 'completed', 'failed' ); @@ -16,5 +16,14 @@ CREATE TABLE framework_exports ( completed_at TIMESTAMP WITH TIME ZONE ); +ALTER TABLE framework_exports ADD CONSTRAINT framework_exports_framework_id_fkey + FOREIGN KEY (framework_id) + REFERENCES frameworks(id) + ON UPDATE CASCADE + ON DELETE CASCADE; - +ALTER TABLE framework_exports ADD CONSTRAINT framework_exports_file_id_fkey + FOREIGN KEY (file_id) + REFERENCES files(id) + ON UPDATE CASCADE + ON DELETE SET NULL; diff --git a/pkg/probo/framework_service.go b/pkg/probo/framework_service.go index 59891f8e8..76b01518e 100644 --- a/pkg/probo/framework_service.go +++ b/pkg/probo/framework_service.go @@ -71,8 +71,10 @@ type ( func (s FrameworkService) RequestExport( ctx context.Context, frameworkID gid.GID, -) error { - return s.svc.pg.WithTx(ctx, func(conn pg.Conn) error { +) (error, *coredata.FrameworkExport) { + frameworkExport := &coredata.FrameworkExport{} + + err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error { framework := &coredata.Framework{} if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil { return fmt.Errorf("cannot load framework: %w", err) @@ -80,7 +82,7 @@ func (s FrameworkService) RequestExport( now := time.Now() - frameworkExport := &coredata.FrameworkExport{ + frameworkExport = &coredata.FrameworkExport{ ID: gid.New(framework.ID.TenantID(), coredata.FrameworkExportEntityType), FrameworkID: frameworkID, Status: coredata.FrameworkExportStatusPending, @@ -93,6 +95,12 @@ func (s FrameworkService) RequestExport( return nil }) + + if err != nil { + return err, nil + } + + return nil, frameworkExport } func (s FrameworkService) Export( diff --git a/pkg/probo/service.go b/pkg/probo/service.go index a8f3755c4..903e17dd1 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -201,6 +201,13 @@ func (s *Service) ExportFrameworkJob(ctx context.Context) error { } scope := coredata.NewScope(fe.ID.TenantID()) + + fe.Status = coredata.FrameworkExportStatusProcessing + fe.StartedAt = ref.Ref(time.Now()) + if err := fe.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update framework export: %w", err) + } + framework := &coredata.Framework{} if err := framework.LoadByID(ctx, tx, scope, fe.FrameworkID); err != nil { fe.Status = coredata.FrameworkExportStatusFailed @@ -209,9 +216,7 @@ func (s *Service) ExportFrameworkJob(ctx context.Context) error { return fmt.Errorf("cannot update framework export: %w", err) } - // s.logger.Error(ctx, "cannot load framework", "error", err) - - return nil + return fmt.Errorf("cannot load framework: %w", err) } tenantService := s.WithTenant(fe.ID.TenantID()) @@ -225,9 +230,7 @@ func (s *Service) ExportFrameworkJob(ctx context.Context) error { return fmt.Errorf("cannot update framework export: %w", err) } - // s.logger.Error(ctx, "cannot create temp file", "error", err) - - return nil + return fmt.Errorf("cannot create temp file: %w", err) } defer tempFile.Close() defer os.Remove(tempFile.Name()) @@ -240,8 +243,7 @@ func (s *Service) ExportFrameworkJob(ctx context.Context) error { return fmt.Errorf("cannot update framework export: %w", err) } - // s.logger.Error(ctx, "cannot export framework", "error", err) - return nil + return fmt.Errorf("cannot export framework: %w", err) } uuid, err := uuid.NewV4() @@ -252,8 +254,7 @@ func (s *Service) ExportFrameworkJob(ctx context.Context) error { return fmt.Errorf("cannot update framework export: %w", err) } - // s.logger.Error(ctx, "cannot generate UUID", "error", err) - return nil + return fmt.Errorf("cannot update framework export: %w", err) } if _, err := tempFile.Seek(0, 0); err != nil { @@ -263,8 +264,7 @@ func (s *Service) ExportFrameworkJob(ctx context.Context) error { return fmt.Errorf("cannot update framework export: %w", err) } - // s.logger.Error(ctx, "cannot seek temp file", "error", err) - return nil + return fmt.Errorf("cannot seek temp file: %w", err) } fileInfo, err := tempFile.Stat() @@ -275,8 +275,7 @@ func (s *Service) ExportFrameworkJob(ctx context.Context) error { return fmt.Errorf("cannot update framework export: %w", err) } - // s.logger.Error(ctx, "cannot get temp file info", "error", err) - return nil + return fmt.Errorf("cannot stat temp file: %w", err) } _, err = s.s3.PutObject( @@ -300,8 +299,7 @@ func (s *Service) ExportFrameworkJob(ctx context.Context) error { return fmt.Errorf("cannot update framework export: %w", err) } - // s.logger.Error(ctx, "cannot upload file to S3", "error", err) - return nil + return fmt.Errorf("cannot upload file to S3: %w", err) } now := time.Now() @@ -312,6 +310,7 @@ func (s *Service) ExportFrameworkJob(ctx context.Context) error { MimeType: "application/zip", FileName: fmt.Sprintf("%s Archive %s.zip", framework.Name, time.Now().Format("2006-01-02")), FileKey: uuid.String(), + FileSize: int(fileInfo.Size()), CreatedAt: now, UpdatedAt: now, } diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index ab792551f..0949f9e8f 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -330,9 +330,19 @@ func (impl *Implm) Run( } }() + frameworkExporterCtx, stopFrameworkExporter := context.WithCancel(context.Background()) + wg.Add(1) + go func() { + defer wg.Done() + if err := impl.runFrameworkExporter(frameworkExporterCtx, proboService, l.Named("framework-exporter")); err != nil { + cancel(fmt.Errorf("framework exporter crashed: %w", err)) + } + }() + <-ctx.Done() stopMailer() + stopFrameworkExporter() stopApiServer() wg.Wait() @@ -342,6 +352,26 @@ func (impl *Implm) Run( return context.Cause(ctx) } +func (impl *Implm) runFrameworkExporter( + ctx context.Context, + proboService *probo.Service, + l *log.Logger, +) error { +LOOP: + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(30 * time.Second): + if err := proboService.ExportFrameworkJob(ctx); err != nil { + if !errors.Is(err, coredata.ErrNoFrameworkExportAvailable) { + l.ErrorCtx(ctx, "cannot process framework export", log.Error(err)) + } + } + + goto LOOP + } +} + func (impl *Implm) runApiServer( ctx context.Context, l *log.Logger, diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 790594c69..11ed6d1f7 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1680,7 +1680,15 @@ func (r *mutationResolver) GenerateFrameworkStateOfApplicability(ctx context.Con // ExportFramework is the resolver for the exportFramework field. func (r *mutationResolver) ExportFramework(ctx context.Context, input types.ExportFrameworkInput) (*types.ExportFrameworkPayload, error) { - panic(fmt.Errorf("not implemented: ExportFramework - exportFramework")) + prb := r.ProboService(ctx, input.FrameworkID.TenantID()) + err, exportJobID := prb.Frameworks.RequestExport(ctx, input.FrameworkID) + if err != nil { + return nil, fmt.Errorf("cannot export framework: %w", err) + } + + return &types.ExportFrameworkPayload{ + ExportJobID: exportJobID.ID, + }, nil } // CreateControl is the resolver for the createControl field. @@ -3963,10 +3971,6 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error return types.NewVendor(vendor), nil case coredata.FrameworkEntityType: - if err := prb.Frameworks.Export(ctx, id, nil); err != nil { - panic(fmt.Errorf("cannot export organization frameworks: %w", err)) - } - framework, err := prb.Frameworks.Get(ctx, id) if err != nil { panic(fmt.Errorf("cannot get framework: %w", err))