diff --git a/api/server/handlers/porter_app/validate.go b/api/server/handlers/porter_app/validate.go index 4ccb67bca4..2e9cc7f48e 100644 --- a/api/server/handlers/porter_app/validate.go +++ b/api/server/handlers/porter_app/validate.go @@ -38,10 +38,11 @@ func NewValidatePorterAppHandler( // Deletions are the names of services and env variables to delete type Deletions struct { - ServiceNames []string `json:"service_names"` - Predeploy []string `json:"predeploy"` - EnvVariableNames []string `json:"env_variable_names"` - EnvGroupNames []string `json:"env_group_names"` + ServiceNames []string `json:"service_names"` + Predeploy []string `json:"predeploy"` + EnvVariableNames []string `json:"env_variable_names"` + EnvGroupNames []string `json:"env_group_names"` + DomainNameDeletions map[string][]string `json:"domain_name_deletions"` } // ValidatePorterAppRequest is the request object for the /apps/validate endpoint @@ -143,6 +144,16 @@ func (c *ValidatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "validated-with-overrides", Value: true}) } + var ServiceDomainsDeletions map[string]*porterv1.DomainNameList + if request.Deletions.DomainNameDeletions != nil { + ServiceDomainsDeletions = make(map[string]*porterv1.DomainNameList) + for k, v := range request.Deletions.DomainNameDeletions { + ServiceDomainsDeletions[k] = &porterv1.DomainNameList{ + DomainNames: v, + } + } + } + validateReq := connect.NewRequest(&porterv1.ValidatePorterAppRequest{ ProjectId: int64(project.ID), DeploymentTargetId: request.DeploymentTargetId, @@ -154,6 +165,7 @@ func (c *ValidatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ PredeployNames: request.Deletions.Predeploy, EnvVariableNames: request.Deletions.EnvVariableNames, EnvGroupNames: request.Deletions.EnvGroupNames, + ServiceDomains: ServiceDomainsDeletions, }, }) ccpResp, err := c.Config().ClusterControlPlaneClient.ValidatePorterApp(ctx, validateReq) diff --git a/dashboard/src/lib/hooks/useAppValidation.ts b/dashboard/src/lib/hooks/useAppValidation.ts index e049061423..d7213c50ec 100644 --- a/dashboard/src/lib/hooks/useAppValidation.ts +++ b/dashboard/src/lib/hooks/useAppValidation.ts @@ -107,6 +107,16 @@ export const useAppValidation = ({ }) .exhaustive(); + const domainDeletions = data.app.services.reduce( + (acc: Record, svc) => { + if (svc.domainDeletions.length) { + acc[svc.name.value] = svc.domainDeletions.map((d) => d.name); + } + return acc; + }, + {} + ); + const res = await api.validatePorterApp( "", { @@ -122,6 +132,7 @@ export const useAppValidation = ({ predeploy: data.deletions.predeploy.map((s) => s.name), env_group_names: data.deletions.envGroupNames.map((eg) => eg.name), env_variable_names: [], + domain_name_deletions: domainDeletions, }, }, { diff --git a/dashboard/src/lib/porter-apps/index.ts b/dashboard/src/lib/porter-apps/index.ts index 61ec52c117..af2f45e327 100644 --- a/dashboard/src/lib/porter-apps/index.ts +++ b/dashboard/src/lib/porter-apps/index.ts @@ -363,15 +363,15 @@ export function clientAppFromProto({ const predeployOverrides = serializeService(overrides.predeploy); const predeploy = proto.predeploy ? [ - deserializeService({ - service: serializedServiceFromProto({ - name: "pre-deploy", - service: proto.predeploy, - isPredeploy: true, + deserializeService({ + service: serializedServiceFromProto({ + name: "pre-deploy", + service: proto.predeploy, + isPredeploy: true, + }), + override: predeployOverrides, }), - override: predeployOverrides, - }), - ] + ] : undefined; return { diff --git a/dashboard/src/lib/porter-apps/services.ts b/dashboard/src/lib/porter-apps/services.ts index 2e1729dd62..315ef99a15 100644 --- a/dashboard/src/lib/porter-apps/services.ts +++ b/dashboard/src/lib/porter-apps/services.ts @@ -53,12 +53,18 @@ export const serviceValidator = z.object({ allowConcurrent: serviceBooleanValidator.optional(), cron: serviceStringValidator, suspendCron: serviceBooleanValidator.optional(), - timeoutSeconds: serviceNumberValidator + timeoutSeconds: serviceNumberValidator, }), z.object({ type: z.literal("predeploy"), }), ]), + domainDeletions: z + .object({ + name: z.string(), + }) + .array() + .default([]), }); export type ClientService = z.infer; @@ -273,6 +279,7 @@ export function deserializeService({ service.ramMegabytes, override?.ramMegabytes ), + domainDeletions: [], }; return match(service.config) @@ -280,6 +287,13 @@ export function deserializeService({ const overrideWebConfig = override?.config.type == "web" ? override.config : undefined; + const uniqueDomains = Array.from( + new Set([ + ...config.domains.map((domain) => domain.name), + ...(overrideWebConfig?.domains ?? []).map((domain) => domain.name), + ]) + ).map((domain) => ({ name: domain })); + return { ...baseService, config: { @@ -293,9 +307,7 @@ export function deserializeService({ override: overrideWebConfig?.healthCheck, }), - domains: Array.from( - new Set([...config.domains, ...(overrideWebConfig?.domains ?? [])]) - ).map((domain) => ({ + domains: uniqueDomains.map((domain) => ({ name: ServiceField.string( domain.name, overrideWebConfig?.domains.find( @@ -337,15 +349,27 @@ export function deserializeService({ allowConcurrent: typeof config.allowConcurrent === "boolean" || typeof overrideJobConfig?.allowConcurrent === "boolean" - ? ServiceField.boolean(config.allowConcurrent, overrideJobConfig?.allowConcurrent) + ? ServiceField.boolean( + config.allowConcurrent, + overrideJobConfig?.allowConcurrent + ) : ServiceField.boolean(false, undefined), cron: ServiceField.string(config.cron, overrideJobConfig?.cron), suspendCron: typeof config.suspendCron === "boolean" || typeof overrideJobConfig?.suspendCron === "boolean" - ? ServiceField.boolean(config.suspendCron, overrideJobConfig?.suspendCron) + ? ServiceField.boolean( + config.suspendCron, + overrideJobConfig?.suspendCron + ) : ServiceField.boolean(false, undefined), - timeoutSeconds: config.timeoutSeconds == 0 ? ServiceField.number(3600, overrideJobConfig?.timeoutSeconds) : ServiceField.number(config.timeoutSeconds, overrideJobConfig?.timeoutSeconds), + timeoutSeconds: + config.timeoutSeconds == 0 + ? ServiceField.number(3600, overrideJobConfig?.timeoutSeconds) + : ServiceField.number( + config.timeoutSeconds, + overrideJobConfig?.timeoutSeconds + ), }, }; }) @@ -410,6 +434,7 @@ export function serviceProto(service: SerializedService): Service { value: { ...config, allowConcurrentOptional: config.allowConcurrent, + timeoutSeconds: BigInt(config.timeoutSeconds), }, case: "jobConfig", }, @@ -466,14 +491,25 @@ export function serializedServiceFromProto({ ...value, }, })) - .with({ case: "jobConfig" }, ({ value }) => ({ - ...service, - name, - config: { - type: isPredeploy ? ("predeploy" as const) : ("job" as const), - ...value, - allowConcurrent: value.allowConcurrentOptional - }, - })) + .with({ case: "jobConfig" }, ({ value }) => + isPredeploy + ? { + ...service, + name, + config: { + type: "predeploy" as const, + }, + } + : { + ...service, + name, + config: { + type: "job" as const, + ...value, + allowConcurrent: value.allowConcurrentOptional, + timeoutSeconds: Number(value.timeoutSeconds), + }, + } + ) .exhaustive(); } diff --git a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx index 90abdcdbf6..ccd52e1696 100644 --- a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx +++ b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx @@ -329,7 +329,11 @@ const CreateApp: React.FC = ({ history }) => { const msg = "An error occurred while deploying your application. Please try again."; - updateAppStep({ step: "stack-launch-failure", errorMessage: msg, appName: name.value }); + updateAppStep({ + step: "stack-launch-failure", + errorMessage: msg, + appName: name.value, + }); setDeployError(msg); return false; } finally { @@ -373,7 +377,7 @@ const CreateApp: React.FC = ({ history }) => { setStep((prev) => Math.max(prev, 5)); } else { setStep((prev) => Math.min(prev, 2)); - }; + } }, [services]); // todo(ianedwards): it's a bit odd that the button error can be set to either a string or JSX, @@ -606,8 +610,9 @@ const CreateApp: React.FC = ({ history }) => { } > {detectedServices.count > 0 - ? `Detected ${detectedServices.count} service${detectedServices.count > 1 ? "s" : "" - } from porter.yaml.` + ? `Detected ${detectedServices.count} service${ + detectedServices.count > 1 ? "s" : "" + } from porter.yaml.` : `Could not detect any services from porter.yaml. Make sure it exists in the root of your repo.`} diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx index f6ae48da59..d78564fb89 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx @@ -16,6 +16,17 @@ const CustomDomains: React.FC = ({ index }) => { control, name: `app.services.${index}.config.domains`, }); + const { append: appendDomainDeletion } = useFieldArray({ + control, + name: `app.services.${index}.domainDeletions`, + }); + + const onRemove = (i: number, name: string) => { + remove(i); + appendDomainDeletion({ + name, + }); + }; return ( @@ -39,8 +50,9 @@ const CustomDomains: React.FC = ({ index }) => { /> { - //remove customDomain at the index - remove(i); + if (!customDomain.name.readOnly) { + onRemove(i, customDomain.name.value); + } }} > cancel diff --git a/dashboard/src/shared/api.tsx b/dashboard/src/shared/api.tsx index 667f597ef1..94f3bc4051 100644 --- a/dashboard/src/shared/api.tsx +++ b/dashboard/src/shared/api.tsx @@ -912,6 +912,7 @@ const validatePorterApp = baseApi< predeploy: string[]; env_variable_names: string[]; env_group_names: string[]; + domain_name_deletions: Record; }; }, {