diff --git a/.gitignore b/.gitignore index be64ddfae02..19510c231d5 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ node_modules !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +.zed # misc /.sass-cache @@ -66,4 +67,4 @@ testem.log Thumbs.db .nx/cache -.nx/workspace-data \ No newline at end of file +.nx/workspace-data diff --git a/apps/console/src/app/router/main.router.tsx b/apps/console/src/app/router/main.router.tsx index 2c8b7697141..3272b9395f7 100644 --- a/apps/console/src/app/router/main.router.tsx +++ b/apps/console/src/app/router/main.router.tsx @@ -15,6 +15,7 @@ import { PageHelmCreateFeature, PageJobCreateFeature, PageServices, + PageTerraformCreateFeature, } from '@qovery/pages/services' import { PageSettings } from '@qovery/pages/settings' import { PageUser } from '@qovery/pages/user' @@ -44,6 +45,7 @@ import { SERVICES_HELM_TEMPLATE_CREATION_URL, SERVICES_LIFECYCLE_CREATION_URL, SERVICES_LIFECYCLE_TEMPLATE_CREATION_URL, + SERVICES_TERRAFORM_CREATION_URL, SERVICES_URL, SETTINGS_URL, USER_URL, @@ -178,6 +180,12 @@ export const ROUTER: RouterProps[] = [ protected: true, layout: false, }, + { + path: `${SERVICES_URL()}${SERVICES_TERRAFORM_CREATION_URL}/*`, + component: , + protected: true, + layout: false, + }, { path: `${APPLICATION_URL()}/*`, component: , diff --git a/libs/domains/service-terraform/data-access/.eslintrc.json b/libs/domains/service-terraform/data-access/.eslintrc.json new file mode 100644 index 00000000000..632e9b0e222 --- /dev/null +++ b/libs/domains/service-terraform/data-access/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/domains/service-terraform/data-access/README.md b/libs/domains/service-terraform/data-access/README.md new file mode 100644 index 00000000000..380a07af02f --- /dev/null +++ b/libs/domains/service-terraform/data-access/README.md @@ -0,0 +1,3 @@ +# domains-service-terraform-data-access + +This library was generated with [Nx](https://nx.dev). diff --git a/libs/domains/service-terraform/data-access/project.json b/libs/domains/service-terraform/data-access/project.json new file mode 100644 index 00000000000..021e34982c3 --- /dev/null +++ b/libs/domains/service-terraform/data-access/project.json @@ -0,0 +1,9 @@ +{ + "name": "domains-service-terraform-data-access", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/domains/service-terraform/data-access/src", + "projectType": "library", + "tags": ["scope:domain", "type:data-access", "slice:services"], + "// targets": "to see all targets run: nx show project domains-service-terraform-data-access --web", + "targets": {} +} diff --git a/libs/domains/service-terraform/data-access/src/index.ts b/libs/domains/service-terraform/data-access/src/index.ts new file mode 100644 index 00000000000..863eb222c60 --- /dev/null +++ b/libs/domains/service-terraform/data-access/src/index.ts @@ -0,0 +1 @@ +export * from './lib/domains-service-terraform-data-access' diff --git a/libs/domains/service-terraform/data-access/src/lib/domains-service-terraform-data-access.ts b/libs/domains/service-terraform/data-access/src/lib/domains-service-terraform-data-access.ts new file mode 100644 index 00000000000..96e80643e80 --- /dev/null +++ b/libs/domains/service-terraform/data-access/src/lib/domains-service-terraform-data-access.ts @@ -0,0 +1,16 @@ +import { type TerraformRequest, TerraformsApi } from 'qovery-typescript-axios' + +const terraformsApi = new TerraformsApi() + +export const mutations = { + async createTerraformService({ + environmentId, + terraformRequest, + }: { + environmentId: string + terraformRequest: TerraformRequest + }) { + const response = await terraformsApi.createTerraform(environmentId, terraformRequest) + return response.data + }, +} diff --git a/libs/domains/service-terraform/data-access/tsconfig.json b/libs/domains/service-terraform/data-access/tsconfig.json new file mode 100644 index 00000000000..059cd816618 --- /dev/null +++ b/libs/domains/service-terraform/data-access/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/domains/service-terraform/data-access/tsconfig.lib.json b/libs/domains/service-terraform/data-access/tsconfig.lib.json new file mode 100644 index 00000000000..18f2d37a19a --- /dev/null +++ b/libs/domains/service-terraform/data-access/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/domains/service-terraform/feature/.babelrc b/libs/domains/service-terraform/feature/.babelrc new file mode 100644 index 00000000000..1ea870ead41 --- /dev/null +++ b/libs/domains/service-terraform/feature/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/domains/service-terraform/feature/.eslintrc.json b/libs/domains/service-terraform/feature/.eslintrc.json new file mode 100644 index 00000000000..772a43d2783 --- /dev/null +++ b/libs/domains/service-terraform/feature/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/domains/service-terraform/feature/README.md b/libs/domains/service-terraform/feature/README.md new file mode 100644 index 00000000000..224cbc3c917 --- /dev/null +++ b/libs/domains/service-terraform/feature/README.md @@ -0,0 +1,7 @@ +# domains-service-terraform-feature + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test domains-service-terraform-feature` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/domains/service-terraform/feature/jest.config.ts b/libs/domains/service-terraform/feature/jest.config.ts new file mode 100644 index 00000000000..7ba31a6c900 --- /dev/null +++ b/libs/domains/service-terraform/feature/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'domains-service-helm-feature', + preset: '../../../../jest.preset.js', + transform: { + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', + '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../../coverage/libs/domains/service-helm/feature', +} diff --git a/libs/domains/service-terraform/feature/project.json b/libs/domains/service-terraform/feature/project.json new file mode 100644 index 00000000000..146bf84581a --- /dev/null +++ b/libs/domains/service-terraform/feature/project.json @@ -0,0 +1,21 @@ +{ + "name": "domains-service-terraform-feature", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/domains/service-terraform/feature/src", + "projectType": "library", + "tags": ["scope:domain", "type:feature", "slice:service-terraform"], + "implicitDependencies": ["!shared-console-shared"], + "targets": { + "test": { + "options": { + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "coverage": true + } + } + } + } +} diff --git a/libs/domains/service-terraform/feature/src/index.ts b/libs/domains/service-terraform/feature/src/index.ts new file mode 100644 index 00000000000..a29a794d8a8 --- /dev/null +++ b/libs/domains/service-terraform/feature/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/hooks/use-create-terraform-service/use-create-terraform-service' +export * from './source-setting/source-setting' +export * from './values-override-arguments-setting/values-override-arguments-setting' diff --git a/libs/domains/service-terraform/feature/src/lib/hooks/use-create-terraform-service/use-create-terraform-service.ts b/libs/domains/service-terraform/feature/src/lib/hooks/use-create-terraform-service/use-create-terraform-service.ts new file mode 100644 index 00000000000..e012624f5de --- /dev/null +++ b/libs/domains/service-terraform/feature/src/lib/hooks/use-create-terraform-service/use-create-terraform-service.ts @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { mutations } from '@qovery/domains/service-terraform/data-access' +import { queries } from '@qovery/state/util-queries' + +export function useCreateTerraformService() { + const queryClient = useQueryClient() + + return useMutation(mutations.createTerraformService, { + onSuccess(_, { environmentId }) { + queryClient.invalidateQueries({ + queryKey: queries.services.list(environmentId).queryKey, + }) + }, + meta: { + notifyOnSuccess: { + title: 'Your Terraform service has been created', + }, + notifyOnError: true, + }, + }) +} + +export default useCreateTerraformService diff --git a/libs/domains/service-terraform/feature/src/source-setting/source-setting.tsx b/libs/domains/service-terraform/feature/src/source-setting/source-setting.tsx new file mode 100644 index 00000000000..6f603ae1334 --- /dev/null +++ b/libs/domains/service-terraform/feature/src/source-setting/source-setting.tsx @@ -0,0 +1,38 @@ +import { Controller, useFormContext } from 'react-hook-form' +import { InputSelect } from '@qovery/shared/ui' + +export function SourceSetting() { + const { control, resetField } = useFormContext() + + return ( +
+ ( + { + resetField('repository') + field.onChange(value) + }} + value={field.value} + error={error?.message} + /> + )} + /> +
+ ) +} + +export default SourceSetting diff --git a/libs/domains/service-terraform/feature/src/values-override-arguments-setting/values-override-arguments-setting.tsx b/libs/domains/service-terraform/feature/src/values-override-arguments-setting/values-override-arguments-setting.tsx new file mode 100644 index 00000000000..0de2d0da0dc --- /dev/null +++ b/libs/domains/service-terraform/feature/src/values-override-arguments-setting/values-override-arguments-setting.tsx @@ -0,0 +1,219 @@ +import { type HelmRequestAllOfSource } from 'qovery-typescript-axios' +import { type PropsWithChildren } from 'react' +import { + Controller, + type UseFieldArrayRemove, + type UseFormReturn, + useFieldArray, + useFormContext, +} from 'react-hook-form' +import { useParams } from 'react-router-dom' +import { FieldVariableSuggestion } from '@qovery/domains/variables/feature' +import { Button, Heading, Icon, InputTextSmall, Section } from '@qovery/shared/ui' + +export interface TerraformValuesArgumentsData { + tf_var_file_paths: string[] + tf_vars: { + key: string + value: string + }[] +} + +export interface ValuesOverrideArgumentsSettingProps extends PropsWithChildren { + source: HelmRequestAllOfSource + methods: UseFormReturn + onSubmit: () => void +} + +function VarRow({ index, remove }: { index: number; remove: UseFieldArrayRemove }) { + const { environmentId = '' } = useParams() + const { control } = useFormContext() + + return ( +
  • +
    + ( + + )} + /> + + ( + + )} + /> + +
    + +
    +
    +
  • + ) +} + +function PathRow({ + field, + index, + tfPathsRemove, +}: { + field: Record<'id', string> + index: number + tfPathsRemove: (index: number) => void +}) { + const { control } = useFormContext() + + return ( +
  • +
    + ( + + )} + /> + + +
    +
  • + ) +} + +export function ValuesOverrideArgumentsSetting({ methods, children, onSubmit }: ValuesOverrideArgumentsSettingProps) { + const { + fields: tfVars, + append: tfVarsAppend, + remove: tfVarsRemove, + } = useFieldArray({ + control: methods.control, + name: 'tf_vars', + }) + const { + fields: tfPaths, + append: tfPathsAppend, + remove: tfPathsRemove, + } = useFieldArray({ + name: 'tf_var_file_paths', + }) + + return ( +
    +
    +
    +
    +
    +
    + Variables +

    Specify each variable by declaring its key and value.

    +
    + + +
    + {tfVars.length > 0 && ( +
      +
    • + Key + Value + +
    • + {tfVars.map((field, index) => ( + + ))} +
    + )} +
    + +
    +
    +
    + File paths +

    Specify each path by declaring its value.

    +
    + + +
    + {tfPaths.length > 0 && ( +
      +
    • + Path +
    • + {tfPaths.map((field, index) => ( + + ))} +
    + )} +
    +
    + + {children} +
    +
    + ) +} + +export default ValuesOverrideArgumentsSetting diff --git a/libs/domains/service-terraform/feature/tsconfig.json b/libs/domains/service-terraform/feature/tsconfig.json new file mode 100644 index 00000000000..c88d07daddd --- /dev/null +++ b/libs/domains/service-terraform/feature/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../../tsconfig.base.json" +} diff --git a/libs/domains/service-terraform/feature/tsconfig.lib.json b/libs/domains/service-terraform/feature/tsconfig.lib.json new file mode 100644 index 00000000000..1547ea009d1 --- /dev/null +++ b/libs/domains/service-terraform/feature/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" + ], + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/domains/service-terraform/feature/tsconfig.spec.json b/libs/domains/service-terraform/feature/tsconfig.spec.json new file mode 100644 index 00000000000..1033686367b --- /dev/null +++ b/libs/domains/service-terraform/feature/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/domains/services/data-access/src/lib/domains-services-data-access.ts b/libs/domains/services/data-access/src/lib/domains-services-data-access.ts index b1edafe478f..91113c21c7b 100644 --- a/libs/domains/services/data-access/src/lib/domains-services-data-access.ts +++ b/libs/domains/services/data-access/src/lib/domains-services-data-access.ts @@ -12,6 +12,7 @@ import { ApplicationMainCallsApi, type ApplicationRequest, ApplicationsApi, + type CheckedCustomDomainResponse, type CleanFailedJobsRequest, ContainerActionsApi, type ContainerAdvancedSettings, @@ -59,14 +60,23 @@ import { JobsApi, type RebootServicesRequest, type Status, + TerraformActionsApi, + type TerraformAdvancedSettings, TerraformConfigurationApi, + type TerraformDeployRequest, + TerraformDeploymentHistoryApi, + TerraformDeploymentRestrictionApi, + type TerraformDeploymentRestrictionRequest, TerraformMainCallsApi, + type TerraformRequest, + TerraformsApi, type Application as _Application, type CloneServiceRequest as _CloneServiceRequest, type ContainerResponse as _Container, type Database as _Database, type HelmResponse as _Helm, type JobResponse as _Job, + type TerraformResponse as _Terraform, } from 'qovery-typescript-axios' import { type ApplicationStatusDto, type DatabaseStatusDto, type ServiceMetricsDto } from 'qovery-ws-typescript-axios' import { match } from 'ts-pattern' @@ -80,6 +90,7 @@ const containersApi = new ContainersApi() const databasesApi = new DatabasesApi() const jobsApi = new JobsApi() const helmsApi = new HelmsApi() +const terraformsApi = new TerraformsApi() const applicationMainCallsApi = new ApplicationMainCallsApi() const containerMainCallsApi = new ContainerMainCallsApi() @@ -92,17 +103,20 @@ const terraformMainCallsApi = new TerraformMainCallsApi() const applicationDeploymentRestrictionApi = new ApplicationDeploymentRestrictionApi() const jobDeploymentRestrictionApi = new JobDeploymentRestrictionApi() const helmDeploymentRestrictionApi = new HelmDeploymentRestrictionApi() +const terraformDeploymentRestrictionApi = new TerraformDeploymentRestrictionApi() const applicationDeploymentsApi = new ApplicationDeploymentHistoryApi() const containerDeploymentsApi = new ContainerDeploymentHistoryApi() const databaseDeploymentsApi = new DatabaseDeploymentHistoryApi() const helmDeploymentsApi = new HelmDeploymentHistoryApi() +const terraformDeploymentsApi = new TerraformDeploymentHistoryApi() const jobDeploymentsApi = new JobDeploymentHistoryApi() const applicationActionsApi = new ApplicationActionsApi() const containerActionsApi = new ContainerActionsApi() const databaseActionsApi = new DatabaseActionsApi() const helmActionsApi = new HelmActionsApi() +const terraformActionsApi = new TerraformActionsApi() const jobActionsApi = new JobActionsApi() const applicationConfigurationApi = new ApplicationConfigurationApi() @@ -128,6 +142,7 @@ export type ContainerType = Extract export type DatabaseType = Extract export type JobType = Extract export type HelmType = Extract +export type TerraformType = Extract // XXX: Need to remove `serviceType` and use only `service_type` since the the API now supports it. // Waiting to have this implementation available in the edition interfaces. @@ -151,8 +166,12 @@ export type Helm = _Helm & { // @deprecated Prefer use `service_type` from API instead of `serviceType` serviceType: HelmType } +export type Terraform = _Terraform & { + // @deprecated Prefer use `service_type` from API instead of `serviceType` + serviceType: TerraformType +} -export type AnyService = Application | Database | Container | Job | Helm +export type AnyService = Application | Database | Container | Job | Helm | Terraform export type AdvancedSettings = | ApplicationAdvancedSettings @@ -276,8 +295,10 @@ export const services = createQueryKeys('services', { .with('HELM', () => helmDeploymentRestrictionApi.getHelmDeploymentRestrictions.bind(helmDeploymentRestrictionApi) ) + .with('TERRAFORM', () => + terraformDeploymentRestrictionApi.getTerraformDeploymentRestrictions.bind(terraformDeploymentRestrictionApi) + ) .with('CONTAINER', 'DATABASE', () => null) - .with('TERRAFORM', () => null) // TODO [QOV-821] double check that .exhaustive() if (!fn) { throw new Error(`deploymentRestrictions unsupported for serviceType: ${serviceType}`) @@ -322,7 +343,7 @@ export const services = createQueryKeys('services', { props: | { serviceId: string - serviceType: Extract + serviceType: Extract } | { serviceId: string @@ -352,6 +373,12 @@ export const services = createQueryKeys('services', { serviceType, } }) + .with({ serviceType: 'TERRAFORM' }, async ({ serviceId, serviceType }) => { + return { + results: (await terraformMainCallsApi.listTerraformCommit(serviceId)).data.results, + serviceType, + } + }) .exhaustive() return commits }, @@ -379,7 +406,10 @@ export const services = createQueryKeys('services', { async () => (await jobDeploymentsApi.listJobDeploymentHistoryV2(serviceId)).data.results ) .with('HELM', async () => (await helmDeploymentsApi.listHelmDeploymentHistoryV2(serviceId)).data.results) - .with('TERRAFORM', async () => undefined) // TODO [QOV-821] to be implemented + .with( + 'TERRAFORM', + async () => (await terraformDeploymentsApi.listTerraformDeploymentHistoryV2(serviceId)).data.results + ) .exhaustive() }, }), @@ -448,7 +478,7 @@ export const services = createQueryKeys('services', { })) .with('HELM', (serviceType) => ({ query: helmsApi.getDefaultHelmAdvancedSettings.bind(helmsApi), serviceType })) .with('TERRAFORM', (serviceType) => ({ - query: async () => ({ data: {} }), // TODO [QOV-821] to be implemented + query: terraformsApi.getDefaultTerraformAdvancedSettings.bind(terraformConfigurationApi), serviceType, })) .exhaustive() @@ -485,7 +515,7 @@ export const services = createQueryKeys('services', { .with('TERRAFORM', (serviceType) => ({ query: terraformConfigurationApi.getTerraformAdvancedSettings.bind(terraformConfigurationApi), serviceType, - })) // TODO [QOV-821] to double check + })) .exhaustive() const response = await query(serviceId) return response.data @@ -496,7 +526,7 @@ export const services = createQueryKeys('services', { serviceType, }: { serviceId: string - serviceType: Extract + serviceType: Extract }) => ({ queryKey: [serviceId], async queryFn() { @@ -513,6 +543,10 @@ export const services = createQueryKeys('services', { query: customDomainHelmApi.listHelmCustomDomain.bind(customDomainHelmApi), serviceType, })) + .with('TERRAFORM', (serviceType) => ({ + query: customDomainHelmApi.listHelmCustomDomain.bind(customDomainHelmApi), + serviceType, + })) // TODO [QOV-821] replace with customDomainTerraformApi when it will be available .exhaustive() const response = await query(serviceId) return response.data.results @@ -523,7 +557,7 @@ export const services = createQueryKeys('services', { serviceType, }: { serviceId: string - serviceType: Extract + serviceType: Extract }) => ({ queryKey: [serviceId], async queryFn() { @@ -540,6 +574,10 @@ export const services = createQueryKeys('services', { query: customDomainHelmApi.checkHelmCustomDomain.bind(customDomainHelmApi), serviceType, })) + .with('TERRAFORM', (serviceType) => ({ + query: async () => ({ data: { results: [] as CheckedCustomDomainResponse[] } }), // TODO [QOV-821] to be implemented + serviceType, + })) .exhaustive() const response = await query(serviceId) return response.data.results @@ -572,6 +610,12 @@ type DeploymentRestrictionRequest = deploymentRestrictionId: string payload: HelmDeploymentRestrictionRequest } + | { + serviceId: string + serviceType: TerraformType + deploymentRestrictionId: string + payload: TerraformDeploymentRestrictionRequest + } type CreateServiceRequest = { environmentId: string @@ -591,6 +635,9 @@ type CreateServiceRequest = { | ({ serviceType: HelmType } & HelmRequest) + | ({ + serviceType: TerraformType + } & TerraformRequest) } type EditServiceRequest = { @@ -611,6 +658,9 @@ type EditServiceRequest = { | ({ serviceType: HelmType } & HelmRequest) + | ({ + serviceType: TerraformType + } & TerraformRequest) } type DeployRequest = @@ -639,6 +689,11 @@ type DeployRequest = serviceId: string serviceType: DatabaseType } + | { + serviceId: string + serviceType: TerraformType + request?: TerraformDeployRequest + } type EditAdvancedSettingsRequest = { serviceId: string @@ -655,6 +710,9 @@ type EditAdvancedSettingsRequest = { | ({ serviceType: HelmType } & HelmAdvancedSettings) + | ({ + serviceType: TerraformType + } & TerraformAdvancedSettings) } export const mutations = { @@ -672,9 +730,9 @@ export const mutations = { })) .with('HELM', (serviceType) => ({ mutation: helmsApi.cloneHelm.bind(helmsApi), serviceType })) .with('TERRAFORM', (serviceType) => ({ - mutation: () => ({ data: { id: 'id', environment: { id: 'id' } } }), + mutation: terraformsApi.cloneTerraform.bind(terraformsApi), serviceType, - })) // TODO [QOV-821] to be implemented + })) .exhaustive() const response = await mutation(serviceId, payload) return response.data @@ -700,6 +758,12 @@ export const mutations = { mutation: helmDeploymentRestrictionApi.editHelmDeploymentRestriction.bind(helmDeploymentRestrictionApi), serviceType, })) + .with('TERRAFORM', (serviceType) => ({ + mutation: terraformDeploymentRestrictionApi.editTerraformDeploymentRestriction.bind( + terraformDeploymentRestrictionApi + ), + serviceType, + })) .exhaustive() const response = await mutation(serviceId, deploymentRestrictionId, payload) return response.data @@ -724,6 +788,12 @@ export const mutations = { mutation: helmDeploymentRestrictionApi.createHelmDeploymentRestriction.bind(helmDeploymentRestrictionApi), serviceType, })) + .with('TERRAFORM', (serviceType) => ({ + mutation: terraformDeploymentRestrictionApi.createTerraformDeploymentRestriction.bind( + terraformDeploymentRestrictionApi + ), + serviceType, + })) .exhaustive() const response = await mutation(serviceId, payload) return response.data @@ -748,6 +818,12 @@ export const mutations = { mutation: helmDeploymentRestrictionApi.deleteHelmDeploymentRestriction.bind(helmDeploymentRestrictionApi), serviceType, })) + .with('TERRAFORM', (serviceType) => ({ + mutation: terraformDeploymentRestrictionApi.deleteTerraformDeploymentRestriction.bind( + terraformDeploymentRestrictionApi + ), + serviceType, + })) .exhaustive() const response = await mutation(serviceId, deploymentRestrictionId) return response.data @@ -784,7 +860,7 @@ export const mutations = { .with('TERRAFORM', (serviceType) => ({ mutation: terraformMainCallsApi.deleteTerraform.bind(terraformMainCallsApi), serviceType, - })) // TODO [QOV-821] double check that + })) .exhaustive() const response = await mutation(serviceId) return response.data @@ -811,6 +887,11 @@ export const mutations = { mutation: helmsApi.createHelm.bind(helmsApi, environmentId, payload), serviceType: 'HELM' as const, })) + .with({ serviceType: 'TERRAFORM' }, (payload) => ({ + mutation: terraformsApi.createTerraform.bind(terraformsApi, environmentId, payload), + serviceType: 'TERRAFORM' as const, + })) + .exhaustive() const response = await mutation() return response.data @@ -837,6 +918,10 @@ export const mutations = { mutation: helmMainCallsApi.editHelm.bind(helmMainCallsApi, serviceId, payload), serviceType, })) + .with({ serviceType: 'TERRAFORM' }, ({ serviceType, ...payload }) => ({ + mutation: terraformMainCallsApi.editTerraform.bind(terraformMainCallsApi, serviceId, payload), + serviceType, + })) .exhaustive() const response = await mutation() return response.data @@ -901,6 +986,10 @@ export const mutations = { mutation: helmActionsApi.deployHelm.bind(helmActionsApi, serviceId, undefined, request), serviceType, })) + .with({ serviceType: 'TERRAFORM' }, ({ serviceId, serviceType, request }) => ({ + mutation: terraformActionsApi.deployTerraform.bind(terraformActionsApi, serviceId, request), + serviceType, + })) .exhaustive() const response = await mutation() return response.data @@ -976,6 +1065,14 @@ export const mutations = { mutation: helmConfigurationApi.editHelmAdvancedSettings.bind(helmConfigurationApi, serviceId, payload), serviceType, })) + .with({ serviceType: 'TERRAFORM' }, ({ serviceType, ...payload }) => ({ + mutation: terraformConfigurationApi.editTerraformAdvancedSettings.bind( + terraformConfigurationApi, + serviceId, + payload + ), + serviceType, + })) .exhaustive() const response = await mutation() return response.data diff --git a/libs/domains/services/feature/src/lib/hooks/use-deployment-restrictions/use-deployment-restrictions.ts b/libs/domains/services/feature/src/lib/hooks/use-deployment-restrictions/use-deployment-restrictions.ts index b3d0d361935..5d003b258f7 100644 --- a/libs/domains/services/feature/src/lib/hooks/use-deployment-restrictions/use-deployment-restrictions.ts +++ b/libs/domains/services/feature/src/lib/hooks/use-deployment-restrictions/use-deployment-restrictions.ts @@ -1,10 +1,15 @@ import { useQuery } from '@tanstack/react-query' -import { type ApplicationType, type HelmType, type JobType } from '@qovery/domains/services/data-access' +import { + type ApplicationType, + type HelmType, + type JobType, + type TerraformType, +} from '@qovery/domains/services/data-access' import { queries } from '@qovery/state/util-queries' export interface UseDeploymentRestrictionsProps { serviceId: string - serviceType: ApplicationType | JobType | HelmType + serviceType: ApplicationType | JobType | HelmType | TerraformType } export function useDeploymentRestrictions({ serviceId, serviceType }: UseDeploymentRestrictionsProps) { diff --git a/libs/domains/services/feature/src/lib/hooks/use-deployment-status/use-deployment-status.ts b/libs/domains/services/feature/src/lib/hooks/use-deployment-status/use-deployment-status.ts index 15376d94493..73c9d770fb8 100644 --- a/libs/domains/services/feature/src/lib/hooks/use-deployment-status/use-deployment-status.ts +++ b/libs/domains/services/feature/src/lib/hooks/use-deployment-status/use-deployment-status.ts @@ -27,6 +27,7 @@ export function useDeploymentStatus({ environmentId, serviceId }: UseDeploymentS ...(environmentStatus?.containers ?? []), ...(environmentStatus?.databases ?? []), ...(environmentStatus?.jobs ?? []), + ...(environmentStatus?.terraforms ?? []), ] return webSocketResult.data ? webSocketResult diff --git a/libs/domains/services/feature/src/lib/last-commit/last-commit.tsx b/libs/domains/services/feature/src/lib/last-commit/last-commit.tsx index 85f0de3242f..c344e6c9639 100644 --- a/libs/domains/services/feature/src/lib/last-commit/last-commit.tsx +++ b/libs/domains/services/feature/src/lib/last-commit/last-commit.tsx @@ -1,6 +1,6 @@ import { type ApplicationGitRepository } from 'qovery-typescript-axios' import { type MouseEvent, useState } from 'react' -import { type Application, type Helm, type Job } from '@qovery/domains/services/data-access' +import { type Application, type Helm, type Job, type Terraform } from '@qovery/domains/services/data-access' import { Button, CopyToClipboard, Icon, Tooltip, Truncate, useModal } from '@qovery/shared/ui' import { useDeployService } from '../hooks/use-deploy-service/use-deploy-service' import { useLastDeployedCommit } from '../hooks/use-last-deployed-commit/use-last-deployed-commit' @@ -9,7 +9,7 @@ import SelectCommitModal from '../select-commit-modal/select-commit-modal' export interface LastCommitProps { organizationId: string projectId: string - service: Pick + service: Pick gitRepository: ApplicationGitRepository } diff --git a/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx b/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx index 5c7a60d7a97..1072b471fc0 100644 --- a/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx +++ b/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx @@ -53,6 +53,7 @@ import { isCancelBuildAvailable, isDeleteAvailable, isDeployAvailable, + isDryRunAvailable, isRedeployAvailable, isRestartAvailable, isStopAvailable, @@ -123,6 +124,10 @@ function MenuManageDeployment({ ) const mutationDeploy = () => deployService({ serviceId: service.id, serviceType: service.serviceType }) + const mutationDryRun = () => { + if (service.serviceType !== 'TERRAFORM') return + deployService({ serviceId: service.id, serviceType: service.serviceType, request: { dry_run: true } }) + } const mutationRedeploy = () => { openModalConfirmation({ @@ -344,6 +349,11 @@ function MenuManageDeployment({ {state === StateEnum.DELETE_QUEUED || state === StateEnum.DELETING ? 'Cancel delete' : 'Cancel deployment'} )} + {isDryRunAvailable(service.serviceType) && ( + } onSelect={mutationDryRun}> + Dry run + + )} {isDeployAvailable(state) && ( } @@ -550,6 +560,7 @@ function MenuManageDeployment({ } ) .with({ service: { serviceType: 'DATABASE' } }, () => null) + .with({ service: { serviceType: 'TERRAFORM' } }, () => null) // TODO [QOV-821] double check that .exhaustive()} {match(service) .with({ serviceType: 'HELM', values_override: P.when(isHelmGitValuesOverride) }, (service) => { diff --git a/libs/domains/services/feature/src/lib/service-avatar/service-avatar.tsx b/libs/domains/services/feature/src/lib/service-avatar/service-avatar.tsx index deacdf1186c..3504f4b26aa 100644 --- a/libs/domains/services/feature/src/lib/service-avatar/service-avatar.tsx +++ b/libs/domains/services/feature/src/lib/service-avatar/service-avatar.tsx @@ -1,4 +1,3 @@ -import { type ServiceTypeEnum } from 'qovery-typescript-axios' import { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react' import { match } from 'ts-pattern' import { type AnyService } from '@qovery/domains/services/data-access' diff --git a/libs/domains/services/feature/src/lib/service-clone-modal/service-clone-modal.tsx b/libs/domains/services/feature/src/lib/service-clone-modal/service-clone-modal.tsx index 29178b1483a..21f21e54837 100644 --- a/libs/domains/services/feature/src/lib/service-clone-modal/service-clone-modal.tsx +++ b/libs/domains/services/feature/src/lib/service-clone-modal/service-clone-modal.tsx @@ -96,6 +96,7 @@ export function ServiceCloneModal({ onClose, organizationId, projectId, serviceI () => 'https://hub.qovery.com/docs/using-qovery/configuration/lifecylejob/#clone' ) .with({ serviceType: 'HELM' }, () => 'https://hub.qovery.com/docs/using-qovery/configuration/helm/#clone') + .with({ serviceType: 'TERRAFORM' }, () => 'https://hub.qovery.com/docs/using-qovery/configuration/terraform/#clone') // TODO [QOV-821] replace URL with new doc path .exhaustive() return ( diff --git a/libs/domains/services/feature/src/lib/service-list/service-list.tsx b/libs/domains/services/feature/src/lib/service-list/service-list.tsx index 7c964db8aa3..97a851812b8 100644 --- a/libs/domains/services/feature/src/lib/service-list/service-list.tsx +++ b/libs/domains/services/feature/src/lib/service-list/service-list.tsx @@ -21,7 +21,13 @@ import type { import { type ComponentProps, Fragment, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { P, match } from 'ts-pattern' -import { type AnyService, type Application, type Helm, type Job } from '@qovery/domains/services/data-access' +import { + type AnyService, + type Application, + type Helm, + type Job, + type Terraform, +} from '@qovery/domains/services/data-access' import { IconEnum, ServiceTypeEnum, @@ -454,7 +460,7 @@ export function ServiceList({ environment, className, ...props }: ServiceListPro cell: (info) => { const service = info.row.original - const gitInfo = (service: Application | Job | Helm, gitRepository?: ApplicationGitRepository) => + const gitInfo = (service: Application | Job | Helm | Terraform, gitRepository?: ApplicationGitRepository) => gitRepository && (
    e.stopPropagation()}>
    @@ -641,6 +647,10 @@ export function ServiceList({ environment, className, ...props }: ServiceListPro }, }) => helmInfo(repository) ) + .with({ service: { serviceType: 'TERRAFORM' } }, ({ service }) => { + // @ts-expect-error Temporary fix for missing type + return gitInfo(service, service?.terraform_files_source?.git?.git_repository) // TODO [CQ-821] double check that + }) .exhaustive() return cell }, diff --git a/libs/pages/application/src/lib/feature/page-settings-deployment-restrictions-feature/crud-modal-feature/crud-modal-feature.tsx b/libs/pages/application/src/lib/feature/page-settings-deployment-restrictions-feature/crud-modal-feature/crud-modal-feature.tsx index 9929823a5c8..ee9cba77e14 100644 --- a/libs/pages/application/src/lib/feature/page-settings-deployment-restrictions-feature/crud-modal-feature/crud-modal-feature.tsx +++ b/libs/pages/application/src/lib/feature/page-settings-deployment-restrictions-feature/crud-modal-feature/crud-modal-feature.tsx @@ -2,17 +2,23 @@ import { type ApplicationDeploymentRestriction, DeploymentRestrictionModeEnum, DeploymentRestrictionTypeEnum, + type TerraformDeploymentRestrictionResponse, } from 'qovery-typescript-axios' import { FormProvider, useForm } from 'react-hook-form' -import { type ApplicationType, type HelmType, type JobType } from '@qovery/domains/services/data-access' +import { + type ApplicationType, + type HelmType, + type JobType, + type TerraformType, +} from '@qovery/domains/services/data-access' import { useCreateDeploymentRestriction, useEditDeploymentRestriction } from '@qovery/domains/services/feature' import CrudModal from '../../../ui/page-settings-deployment-restrictions/crud-modal/crud-modal' export interface CrudModalFeatureProps { - deploymentRestriction?: ApplicationDeploymentRestriction + deploymentRestriction?: ApplicationDeploymentRestriction | TerraformDeploymentRestrictionResponse onClose: () => void serviceId: string - serviceType: ApplicationType | JobType | HelmType + serviceType: ApplicationType | JobType | HelmType | TerraformType } export function CrudModalFeature({ deploymentRestriction, serviceId, serviceType, onClose }: CrudModalFeatureProps) { const serviceParams = { diff --git a/libs/pages/application/src/lib/feature/page-settings-deployment-restrictions-feature/page-settings-deployment-restrictions-feature.tsx b/libs/pages/application/src/lib/feature/page-settings-deployment-restrictions-feature/page-settings-deployment-restrictions-feature.tsx index 13abe3e2c5e..ac2514d4608 100644 --- a/libs/pages/application/src/lib/feature/page-settings-deployment-restrictions-feature/page-settings-deployment-restrictions-feature.tsx +++ b/libs/pages/application/src/lib/feature/page-settings-deployment-restrictions-feature/page-settings-deployment-restrictions-feature.tsx @@ -1,6 +1,14 @@ -import { type ApplicationDeploymentRestriction } from 'qovery-typescript-axios' +import { + type ApplicationDeploymentRestriction, + type TerraformDeploymentRestrictionResponse, +} from 'qovery-typescript-axios' import { useParams } from 'react-router-dom' -import { type ApplicationType, type HelmType, type JobType } from '@qovery/domains/services/data-access' +import { + type ApplicationType, + type HelmType, + type JobType, + type TerraformType, +} from '@qovery/domains/services/data-access' import { useDeleteDeploymentRestriction, useDeploymentRestrictions, @@ -34,7 +42,8 @@ export function PageSettingsDeploymentRestrictionsFeature() { return null } - const isValidServiceType = serviceType === 'APPLICATION' || serviceType === 'JOB' || serviceType === 'HELM' + const isValidServiceType = + serviceType === 'APPLICATION' || serviceType === 'JOB' || serviceType === 'HELM' || serviceType === 'TERRAFORM' return (
    @@ -74,7 +83,7 @@ export function PageSettingsDeploymentRestrictionsFeature() { interface PageSettingsDeploymentRestrictionsFeatureInnerProps { serviceId: string - serviceType: ApplicationType | JobType | HelmType + serviceType: ApplicationType | JobType | HelmType | TerraformType } function PageSettingsDeploymentRestrictionsFeatureInner({ @@ -90,7 +99,9 @@ function PageSettingsDeploymentRestrictionsFeatureInner({ const { mutate: deleteRestriction } = useDeleteDeploymentRestriction() const { openModal, closeModal } = useModal() const { openModalConfirmation } = useModalConfirmation() - const handleEdit = (deploymentRestriction: ApplicationDeploymentRestriction) => { + const handleEdit = ( + deploymentRestriction: ApplicationDeploymentRestriction | TerraformDeploymentRestrictionResponse + ) => { openModal({ content: ( @@ -98,7 +109,9 @@ function PageSettingsDeploymentRestrictionsFeatureInner({ }) } - const handleDelete = (deploymentRestriction: ApplicationDeploymentRestriction) => { + const handleDelete = ( + deploymentRestriction: ApplicationDeploymentRestriction | TerraformDeploymentRestrictionResponse + ) => { openModalConfirmation({ title: 'Delete Restriction', name: `${deploymentRestriction.mode}/${deploymentRestriction.type}/${deploymentRestriction.value}`, diff --git a/libs/pages/application/src/lib/feature/page-settings-feature/page-settings-feature.tsx b/libs/pages/application/src/lib/feature/page-settings-feature/page-settings-feature.tsx index 652be81300e..41d6fcfa988 100644 --- a/libs/pages/application/src/lib/feature/page-settings-feature/page-settings-feature.tsx +++ b/libs/pages/application/src/lib/feature/page-settings-feature/page-settings-feature.tsx @@ -161,6 +161,13 @@ export function PageSettingsFeature() { advancedSettings, dangerzoneSettings, ]) + .with({ serviceType: 'TERRAFORM' }, () => [ + generalSettings, + resourcesSettings, + deploymentRestrictionsSettings, + advancedSettings, + dangerzoneSettings, + ]) .with({ serviceType: 'JOB' }, (s) => [ generalSettings, ...(s.job_type === 'LIFECYCLE' && isJobGitSource(s.source) ? [dockerfileSetting] : []), diff --git a/libs/pages/application/src/lib/feature/page-settings-resources-feature/page-settings-resources-feature.tsx b/libs/pages/application/src/lib/feature/page-settings-resources-feature/page-settings-resources-feature.tsx index 867306c0548..226fe2b0b01 100644 --- a/libs/pages/application/src/lib/feature/page-settings-resources-feature/page-settings-resources-feature.tsx +++ b/libs/pages/application/src/lib/feature/page-settings-resources-feature/page-settings-resources-feature.tsx @@ -22,6 +22,9 @@ export function SettingsResourcesFeature({ service, environment }: SettingsResou const defaultInstances = match(service) .with({ serviceType: 'JOB' }, () => ({})) + .with({ serviceType: 'TERRAFORM' }, (s) => ({ + storage_gib: s.job_resources.storage_gib, + })) .otherwise((s) => ({ min_running_instances: s.min_running_instances || 1, max_running_instances: s.max_running_instances || 1, @@ -30,8 +33,12 @@ export function SettingsResourcesFeature({ service, environment }: SettingsResou const methods = useForm({ mode: 'onChange', defaultValues: { - memory: service.memory, - cpu: service.cpu, + memory: match(service) + .with({ serviceType: 'TERRAFORM' }, (s) => s.job_resources.ram_mib) + .otherwise((s) => s.memory || 0), + cpu: match(service) + .with({ serviceType: 'TERRAFORM' }, (s) => s.job_resources.cpu_milli) + .otherwise((s) => s.cpu || 0), ...defaultInstances, }, }) @@ -67,6 +74,18 @@ export function SettingsResourcesFeature({ service, environment }: SettingsResou request: requestWithInstances, }) ) + .with({ serviceType: 'TERRAFORM' }, (service) => + buildEditServicePayload({ + service, + request: { + job_resources: { + cpu_milli: Number(data['cpu']), + ram_mib: Number(data['memory']), + storage_gib: Number(data['storage_gib']), + }, + }, + }) + ) .exhaustive() editService({ @@ -75,7 +94,8 @@ export function SettingsResourcesFeature({ service, environment }: SettingsResou }) }) - const displayWarningCpu: boolean = (methods.watch('cpu') || 0) > (service.maximum_cpu || 0) + const displayWarningCpu: boolean = + 'maximum_cpu' in service && (methods.watch('cpu') || 0) > (service.maximum_cpu || 0) return ( @@ -98,9 +118,13 @@ export function PageSettingsResourcesFeature() { if (!environment) return null return match(service) - .with({ serviceType: 'APPLICATION' }, { serviceType: 'CONTAINER' }, { serviceType: 'JOB' }, (service) => ( - - )) + .with( + { serviceType: 'APPLICATION' }, + { serviceType: 'CONTAINER' }, + { serviceType: 'JOB' }, + { serviceType: 'TERRAFORM' }, + (service) => + ) .otherwise(() => null) } diff --git a/libs/pages/application/src/lib/ui/page-settings-resources/__snapshots__/page-settings-resources.spec.tsx.snap b/libs/pages/application/src/lib/ui/page-settings-resources/__snapshots__/page-settings-resources.spec.tsx.snap index 27bd64d33ab..e811faecfe8 100644 --- a/libs/pages/application/src/lib/ui/page-settings-resources/__snapshots__/page-settings-resources.spec.tsx.snap +++ b/libs/pages/application/src/lib/ui/page-settings-resources/__snapshots__/page-settings-resources.spec.tsx.snap @@ -84,10 +84,7 @@ exports[`PageSettingsResources should render warning box and icon for cpu 1`] = 10 milli vCPU. - Maximum value allowed based on the selected cluster instance type: - 10 - milli vCPU. - + Maximum value allowed based on the selected cluster instance type: 10 milli vCPU.

    diff --git a/libs/pages/application/src/lib/ui/page-settings-resources/page-settings-resources.spec.tsx b/libs/pages/application/src/lib/ui/page-settings-resources/page-settings-resources.spec.tsx index fdaf4fa0184..5537af2afbc 100644 --- a/libs/pages/application/src/lib/ui/page-settings-resources/page-settings-resources.spec.tsx +++ b/libs/pages/application/src/lib/ui/page-settings-resources/page-settings-resources.spec.tsx @@ -19,6 +19,7 @@ jest.mock('react-hook-form', () => ({ watch: () => jest.fn(), formState: { isValid: true, + isDirty: true, }, }), })) diff --git a/libs/pages/application/src/lib/ui/page-settings-resources/page-settings-resources.tsx b/libs/pages/application/src/lib/ui/page-settings-resources/page-settings-resources.tsx index d1820724239..108c06dae94 100644 --- a/libs/pages/application/src/lib/ui/page-settings-resources/page-settings-resources.tsx +++ b/libs/pages/application/src/lib/ui/page-settings-resources/page-settings-resources.tsx @@ -22,7 +22,7 @@ export function PageSettingsResources(props: PageSettingsResourcesProps) {
    -
    diff --git a/libs/pages/logs/environment/src/lib/feature/deployment-logs-feature/deployment-logs-feature.tsx b/libs/pages/logs/environment/src/lib/feature/deployment-logs-feature/deployment-logs-feature.tsx index bcc94b4d619..947b1c074c2 100644 --- a/libs/pages/logs/environment/src/lib/feature/deployment-logs-feature/deployment-logs-feature.tsx +++ b/libs/pages/logs/environment/src/lib/feature/deployment-logs-feature/deployment-logs-feature.tsx @@ -68,6 +68,13 @@ export function getServiceStatusesById(services?: DeploymentStageWithServicesSta } } } + if (service.terraforms && service.terraforms?.length > 0) { + for (const terraforms of service.terraforms) { + if (terraforms.id === serviceId) { + return terraforms + } + } + } } } return null @@ -84,6 +91,7 @@ export function getStageFromServiceId( 'jobs', 'databases', 'helms', + 'terraforms', ] for (const serviceType of serviceTypes) { diff --git a/libs/pages/logs/environment/src/lib/feature/environment-stages-feature/environment-stages-feature.spec.tsx b/libs/pages/logs/environment/src/lib/feature/environment-stages-feature/environment-stages-feature.spec.tsx index 50b2b83c293..be0e3ac9f04 100644 --- a/libs/pages/logs/environment/src/lib/feature/environment-stages-feature/environment-stages-feature.spec.tsx +++ b/libs/pages/logs/environment/src/lib/feature/environment-stages-feature/environment-stages-feature.spec.tsx @@ -104,6 +104,7 @@ describe('EnvironmentStagesFeature', () => { containers: [], jobs: [], helms: [], + terraforms: [], }, ] diff --git a/libs/pages/logs/environment/src/lib/feature/environment-stages-feature/environment-stages-feature.tsx b/libs/pages/logs/environment/src/lib/feature/environment-stages-feature/environment-stages-feature.tsx index 099d2e2cdb8..c1ae370fa06 100644 --- a/libs/pages/logs/environment/src/lib/feature/environment-stages-feature/environment-stages-feature.tsx +++ b/libs/pages/logs/environment/src/lib/feature/environment-stages-feature/environment-stages-feature.tsx @@ -40,7 +40,7 @@ export function matchServicesWithStatuses(deploymentStages?: DeploymentStageWith if (!deploymentStages) return [] return deploymentStages.map((deploymentStage) => { - const serviceTypes = ['applications', 'databases', 'containers', 'jobs', 'helms'] as const + const serviceTypes = ['applications', 'databases', 'containers', 'jobs', 'helms', 'terraforms'] as const const services = serviceTypes .map((serviceType) => deploymentStage[serviceType]) diff --git a/libs/pages/services/src/index.ts b/libs/pages/services/src/index.ts index 8b16b8f8457..53175460ce9 100644 --- a/libs/pages/services/src/index.ts +++ b/libs/pages/services/src/index.ts @@ -6,3 +6,4 @@ export { PageHelmCreateFeature, type HelmGeneralData, } from './lib/feature/page-helm-create-feature/page-helm-create-feature' +export { PageTerraformCreateFeature } from './lib/feature/page-terraform-create-feature/page-terraform-create-feature' diff --git a/libs/pages/services/src/lib/feature/page-new-feature/page-new-feature.tsx b/libs/pages/services/src/lib/feature/page-new-feature/page-new-feature.tsx index 507e884f725..aaafae09dd1 100644 --- a/libs/pages/services/src/lib/feature/page-new-feature/page-new-feature.tsx +++ b/libs/pages/services/src/lib/feature/page-new-feature/page-new-feature.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx' import TerraformIcon from 'devicon/icons/terraform/terraform-original.svg' import posthog from 'posthog-js' +import { useFeatureFlagEnabled } from 'posthog-js/react' import { type CloudProviderEnum, type LifecycleTemplateListResponseResultsInner } from 'qovery-typescript-axios' import { type ReactElement, cloneElement, useState } from 'react' import { NavLink, useParams } from 'react-router-dom' @@ -18,6 +19,7 @@ import { SERVICES_HELM_TEMPLATE_CREATION_URL, SERVICES_LIFECYCLE_CREATION_URL, SERVICES_LIFECYCLE_TEMPLATE_CREATION_URL, + SERVICES_TERRAFORM_CREATION_URL, SERVICES_URL, } from '@qovery/shared/routes' import { Button, ExternalLink, Heading, Icon, InputSearch, Link, Section } from '@qovery/shared/ui' @@ -260,6 +262,8 @@ export function PageNewFeature() { const { data: environment } = useEnvironment({ environmentId }) const { data: availableTemplates = [] } = useLifecycleTemplates({ environmentId }) + const isTerraformFeatureFlag = Boolean(useFeatureFlagEnabled('terraform')) + const cloudProvider = environment?.cloud_provider.provider as CloudProviderEnum const serviceEmpty = [ @@ -300,6 +304,16 @@ export function PageNewFeature() { }, ] + if (isTerraformFeatureFlag) { + serviceEmpty.push({ + title: 'Terraform', + description: 'Deploy a Terraform configuration on your Kubernetes cluster.', + icon: , + link: SERVICES_URL(organizationId, projectId, environmentId) + SERVICES_TERRAFORM_CREATION_URL, + cloud_provider: cloudProvider, + }) + } + const [searchInput, setSearchInput] = useState('') const filterService = ({ title }: { title: string }) => title.toLowerCase().includes(searchInput.toLowerCase()) diff --git a/libs/pages/services/src/lib/feature/page-settings-preview-environments-feature/page-settings-preview-environments-feature.tsx b/libs/pages/services/src/lib/feature/page-settings-preview-environments-feature/page-settings-preview-environments-feature.tsx index 64855cc3c58..1b7be7b4afc 100644 --- a/libs/pages/services/src/lib/feature/page-settings-preview-environments-feature/page-settings-preview-environments-feature.tsx +++ b/libs/pages/services/src/lib/feature/page-settings-preview-environments-feature/page-settings-preview-environments-feature.tsx @@ -50,6 +50,7 @@ export function SettingsPreviewEnvironmentsFeature({ services }: { services: Any .with({ serviceType: 'CONTAINER' }, (s) => buildEditServicePayload({ service: s, request })) .with({ serviceType: 'JOB' }, (s) => buildEditServicePayload({ service: s, request })) .with({ serviceType: 'HELM' }, (s) => buildEditServicePayload({ service: s, request })) + .with({ serviceType: 'TERRAFORM' }, (s) => buildEditServicePayload({ service: s })) .exhaustive() try { @@ -78,7 +79,7 @@ export function SettingsPreviewEnvironmentsFeature({ services }: { services: Any // Force enable Preview if we enable preview from the 1rst application const toggleEnablePreview = (value: boolean) => { const isApplicationPreviewEnabled = services - ? services.some((service) => service?.serviceType !== 'DATABASE' && service.auto_preview) + ? services.some((service) => 'auto_preview' in service && service.auto_preview) : false if (isApplicationPreviewEnabled || !value) { return @@ -92,13 +93,11 @@ export function SettingsPreviewEnvironmentsFeature({ services }: { services: Any // !loading is here to prevent the toggle to glitch the time we are submitting the two api endpoints if (environmentDeploymentRules && loadingStatusEnvironmentDeploymentRules && !loading) { const isApplicationPreviewEnabled = services - ? services.some((app) => app.serviceType !== 'DATABASE' && app.auto_preview) + ? services.some((app) => 'auto_preview' in app && app.auto_preview) : false methods.setValue('auto_preview', environmentDeploymentRules.auto_preview || isApplicationPreviewEnabled) methods.setValue('on_demand_preview', environmentDeploymentRules.on_demand_preview) - services.forEach((service) => - methods.setValue(service.id, service.serviceType !== 'DATABASE' && service.auto_preview) - ) + services.forEach((service) => methods.setValue(service.id, 'auto_preview' in service && service.auto_preview)) } }, [loadingStatusEnvironmentDeploymentRules, methods, environmentDeploymentRules, services, loading]) diff --git a/libs/pages/services/src/lib/feature/page-terraform-create-feature/page-terraform-create-feature.spec.tsx b/libs/pages/services/src/lib/feature/page-terraform-create-feature/page-terraform-create-feature.spec.tsx new file mode 100644 index 00000000000..cce2da2f969 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-terraform-create-feature/page-terraform-create-feature.spec.tsx @@ -0,0 +1,14 @@ +import { IntercomProvider } from 'react-use-intercom' +import { renderWithProviders } from '@qovery/shared/util-tests' +import PageTerraformCreateFeature from './page-terraform-create-feature' + +describe('PageTerraformCreateFeature', () => { + it('should render successfully', () => { + const { baseElement } = renderWithProviders( + + + + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/services/src/lib/feature/page-terraform-create-feature/page-terraform-create-feature.tsx b/libs/pages/services/src/lib/feature/page-terraform-create-feature/page-terraform-create-feature.tsx new file mode 100644 index 00000000000..c8e94fb3d6f --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-terraform-create-feature/page-terraform-create-feature.tsx @@ -0,0 +1,158 @@ +import { type GitProviderEnum, type GitTokenResponse, type TerraformRequest } from 'qovery-typescript-axios' +import { createContext, useContext, useState } from 'react' +import { type UseFormReturn, useForm } from 'react-hook-form' +import { Navigate, Route, Routes, useNavigate, useParams } from 'react-router-dom' +import { type TerraformValuesArgumentsData } from '@qovery/domains/service-terraform/feature' +import { AssistantTrigger } from '@qovery/shared/assistant/feature' +import { + SERVICES_NEW_URL, + SERVICES_TERRAFORM_CREATION_GENERAL_URL, + SERVICES_TERRAFORM_CREATION_URL, + SERVICES_URL, +} from '@qovery/shared/routes' +import { FunnelFlow } from '@qovery/shared/ui' +import { ROUTER_SERVICE_TERRAFORM_CREATION } from '../../router/router' +import { serviceTemplates } from '../page-new-feature/service-templates' + +export const TERRAFORM_VERSIONS = [ + '1.12.1', + '1.11.4', + '1.10.5', + '1.9.8', + '1.8.5', + '1.7.5', + '1.6.6', + '1.5.7', + '1.4.7', + '1.3.10', + '1.2.9', + '1.1.9', + '1.0.11', + '0.15.5', + '0.14.11', + '0.13.7', + '0.12.31', + '0.11.15', + '0.10.8', + '0.9.11', + '0.8.8', + '0.7.13', + '0.6.16', + '0.5.3', + '0.4.2', + '0.3.7', + '0.2.2', + '0.1.1', +] +export const steps: { title: string }[] = [ + { title: 'General data' }, + { title: 'Values override as file' }, + { title: 'Values override as arguments' }, + { title: 'Summary' }, +] +export interface TerraformGeneralData + extends Omit { + source_provider: 'GIT' + repository: string + is_public_repository?: boolean + provider?: keyof typeof GitProviderEnum + git_token_id?: GitTokenResponse['id'] + branch?: string + root_path?: string + chart_name?: string + chart_version?: string + arguments: string + timeout_sec: string +} + +interface TerraformCreateContextInterface { + currentStep: number + setCurrentStep: (step: number) => void + generalForm: UseFormReturn + valuesOverrideArgumentsForm: UseFormReturn + creationFlowUrl?: string +} + +export const TerraformCreateContext = createContext(undefined) + +// this is to avoid to set initial value twice https://stackoverflow.com/questions/49949099/react-createcontext-point-of-defaultvalue +export const useTerraformCreateContext = () => { + const terraformCreateContext = useContext(TerraformCreateContext) + if (!terraformCreateContext) throw new Error('useTerraformCreateContext must be used within a TerraformCreateContext') + return terraformCreateContext +} + +export function PageTerraformCreateFeature() { + const navigate = useNavigate() + const { organizationId = '', projectId = '', environmentId = '', slug } = useParams() + const [currentStep, setCurrentStep] = useState(1) + + const dataTemplate = serviceTemplates.find((service) => service.slug === slug) + + const generalForm = useForm({ + mode: 'onChange', + defaultValues: { + name: dataTemplate?.slug ?? '', + icon_uri: dataTemplate?.icon_uri ?? 'app://qovery-console/terraform', + source_provider: 'GIT', + provider_version: { + read_from_terraform_block: false, + explicit_version: TERRAFORM_VERSIONS[0], + }, + job_resources: { + cpu_milli: 500, + ram_mib: 256, + storage_gib: 1, + }, + terraform_variables_source: { + tf_vars: [], + tf_var_file_paths: [], + }, + }, + }) + + const valuesOverrideArgumentsForm = useForm({ + mode: 'onChange', + }) + + const creationFlowUrl = SERVICES_URL(organizationId, projectId, environmentId) + SERVICES_TERRAFORM_CREATION_URL + + return ( + + { + if (window.confirm('Do you really want to leave?')) { + const link = `${SERVICES_URL(organizationId, projectId, environmentId)}${SERVICES_NEW_URL}` + navigate(link) + } + }} + totalSteps={steps.length} + currentStep={currentStep} + currentTitle={steps[currentStep - 1].title} + > + + {ROUTER_SERVICE_TERRAFORM_CREATION.map((route) => ( + + ))} + {creationFlowUrl && ( + } + /> + )} + + + + + ) +} + +export default PageTerraformCreateFeature diff --git a/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-configuration-feature/__snapshots__/step-configuration-feature.spec.tsx.snap b/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-configuration-feature/__snapshots__/step-configuration-feature.spec.tsx.snap new file mode 100644 index 00000000000..5e95092ba46 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-configuration-feature/__snapshots__/step-configuration-feature.spec.tsx.snap @@ -0,0 +1,599 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StepConfigurationFeature should submit a form with a git repository 1`] = ` + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + +
    +
    +
    + Select... +
    + +
    +
    + + +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +

    + Amount of CPU allocated to the job resources +

    +
    +
    +
    +
    +
    + + +
    +
    +
    +

    + Amount of RAM allocated to the job resources +

    +
    +
    +
    +
    +
    + + +
    +
    +
    +

    + Amount of storage allocated to the job resources +

    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +

    + Cluster Credentials +

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +`; + +exports[`StepConfigurationFeature should submit a form with a yaml 1`] = ` + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + +
    +
    +
    + Select... +
    + +
    +
    + + +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +

    + Amount of CPU allocated to the job resources +

    +
    +
    +
    +
    +
    + + +
    +
    +
    +

    + Amount of RAM allocated to the job resources +

    +
    +
    +
    +
    +
    + + +
    +
    +
    +

    + Amount of storage allocated to the job resources +

    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +

    + Cluster Credentials +

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +`; diff --git a/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-configuration-feature/step-configuration-feature.spec.tsx b/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-configuration-feature/step-configuration-feature.spec.tsx new file mode 100644 index 00000000000..10f8bdb79aa --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-configuration-feature/step-configuration-feature.spec.tsx @@ -0,0 +1,163 @@ +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' +import { useForm } from 'react-hook-form' +import { type HelmValuesFileData } from '@qovery/domains/service-helm/feature' +import { renderHook, renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests' +import { type TerraformGeneralData } from '../page-terraform-create-feature' +import { TerraformCreateContext } from '../page-terraform-create-feature' +import StepConfigurationFeature from './step-configuration-feature' + +const defaultValues = { + provider_version: { + read_from_terraform_block: false, + explicit_version: undefined, + }, + job_resources: { + cpu_milli: 500, + ram_mib: 256, + storage_gib: 1, + }, +} + +describe('StepConfigurationFeature', () => { + it('should render successfully', () => { + const { result: generalForm } = renderHook(() => + useForm({ + mode: 'onChange', + defaultValues: { + source_provider: 'HELM_REPOSITORY', + repository: 'https://charts.bitnami.com/bitnami', + chart_name: 'nginx', + chart_version: '8.9.0', + arguments: '', + ...defaultValues, + }, + }) + ) + + const { result: valuesOverrideFileForm } = renderHook(() => + useForm({ + mode: 'onChange', + defaultValues: { + type: 'NONE', + }, + }) + ) + + const { baseElement } = renderWithProviders( + + + + ) + expect(baseElement).toBeTruthy() + }) + + it('should submit a form with a git repository', async () => { + const { result: generalForm } = renderHook(() => + useForm({ + mode: 'onChange', + defaultValues: { + source_provider: 'GIT', + provider: 'GITHUB', + repository: 'Qovery/github', + branch: 'main', + root_path: '/', + ...defaultValues, + }, + }) + ) + + const { result: valuesOverrideFileForm } = renderHook(() => + useForm({ + mode: 'onChange', + defaultValues: { + type: 'GIT_REPOSITORY', + provider: 'GITHUB', + repository: 'Qovery/github', + branch: 'main', + paths: '/', + ...defaultValues, + }, + }) + ) + + const { baseElement, userEvent } = renderWithProviders( + wrapWithReactHookForm( + + + + ) + ) + + const button = screen.getByRole('button', { name: 'Continue' }) + + // wait for form to be valid because we have selects (necessary with react hook form) + waitFor(async () => { + expect(button).toBeEnabled() + await userEvent.click(button) + }) + + expect(baseElement).toMatchSnapshot() + }) + + it('should submit a form with a yaml', async () => { + const { result: generalForm } = renderHook(() => + useForm({ + mode: 'onChange', + defaultValues: { + source_provider: 'GIT', + provider: 'GITHUB', + repository: 'Qovery/github', + branch: 'main', + root_path: '/', + ...defaultValues, + }, + }) + ) + + const { result: valuesOverrideFileForm } = renderHook(() => + useForm({ + mode: 'onChange', + defaultValues: { + type: 'YAML', + content: 'test', + }, + }) + ) + + const { baseElement, userEvent } = renderWithProviders( + wrapWithReactHookForm( + + + + ) + ) + + const button = screen.getByRole('button', { name: 'Continue' }) + + expect(button).toBeEnabled() + await userEvent.click(button) + + expect(baseElement).toMatchSnapshot() + }) +}) diff --git a/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-configuration-feature/step-configuration-feature.tsx b/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-configuration-feature/step-configuration-feature.tsx new file mode 100644 index 00000000000..20f73522936 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-configuration-feature/step-configuration-feature.tsx @@ -0,0 +1,139 @@ +import { useEffect } from 'react' +import { Controller, FormProvider } from 'react-hook-form' +import { useNavigate } from 'react-router-dom' +import { + SERVICES_TERRAFORM_CREATION_GENERAL_URL, + SERVICES_TERRAFORM_CREATION_VALUES_STEP_2_URL, +} from '@qovery/shared/routes' +import { Button, FunnelFlowBody, InputSelect, InputText, InputToggle } from '@qovery/shared/ui' +import { useDocumentTitle } from '@qovery/shared/util-hooks' +import { TERRAFORM_VERSIONS, useTerraformCreateContext } from '../page-terraform-create-feature' + +export function StepConfigurationFeature() { + useDocumentTitle('General - Terraform configuration') + + const { generalForm, setCurrentStep, creationFlowUrl } = useTerraformCreateContext() + + const generalData = generalForm.getValues() + + const navigate = useNavigate() + + useEffect(() => { + setCurrentStep(2) + }, [setCurrentStep]) + + const onSubmit = () => { + navigate(creationFlowUrl + SERVICES_TERRAFORM_CREATION_VALUES_STEP_2_URL) + } + + return ( + + + ( + ({ label: v, value: v }))} + /> + )} + /> +
    + +
    + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> +
    + +
    + +
    + ( + + )} + /> +
    + +
    + +
    + +
    +
    +
    +
    + ) +} + +export default StepConfigurationFeature diff --git a/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-general-feature/__snapshots__/step-general-feature.spec.tsx.snap b/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-general-feature/__snapshots__/step-general-feature.spec.tsx.snap new file mode 100644 index 00000000000..adb2474bd5f --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-terraform-create-feature/step-general-feature/__snapshots__/step-general-feature.spec.tsx.snap @@ -0,0 +1,1168 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StepGeneralFeature should submit a form with a git repository 1`] = ` + +
    +
    +
    +
    +
    +
    +

    + General information +

    +

    + These general settings allow you to set up the service name, its source and deployment parameters. +

    + +
    +

    + General +

    +
    +