Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@ for r in patched_deploy:
if "web" in name:
labels = ["web"]
port_forwards = ['5001:80']
elif "gateway" in name:
labels = ["gateway"]
port_forwards = ['5002:80']
elif "worker" in name:
labels = ["worker"]
resource_deps.append('lifecycle-web')
Expand Down
48 changes: 48 additions & 0 deletions helm/environments/default/lifecycle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,54 @@ components:
periodSeconds: 5
failureThreshold: 3

gateway:
enabled: false
fullnameOverride: 'lifecycle-gateway'

service:
enabled: true
type: ClusterIP
port: 80
targetPort: 80

ingress:
enabled: true
ingressClassName: nginx
annotations: {}
hosts:
- host: '*.sites.<your_domain_here>'
paths: ['/']

deployment:
replicaCount: 2
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 500m
memory: 512Mi
extraEnv:
- name: LIFECYCLE_MODE
value: gateway
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 6
readinessProbe:
httpGet:
path: /api/health
port: http
periodSeconds: 5
failureThreshold: 3

postgresql:
enabled: false

Expand Down
50 changes: 49 additions & 1 deletion helm/environments/local/lifecycle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,54 @@ components:
failureThreshold: 10
timeoutSeconds: 10

gateway:
enabled: true
fullnameOverride: 'lifecycle-gateway'
service:
enabled: true
type: ClusterIP
port: 80
targetPort: 80
ingress:
enabled: false
deployment:
replicaCount: 1
extraEnv:
- name: JOB_VERSION
value: default
- name: ENVIRONMENT
value: dev
- name: APP_ENV
value: dev
- name: NODE_ENV
value: development
- name: LIFECYCLE_MODE
value: gateway
- name: PORT
value: '80'
- name: DD_TRACE_ENABLED
value: 'false'
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /api/health
port: 80
initialDelaySeconds: 5
periodSeconds: 30
failureThreshold: 6
timeoutSeconds: 60
readinessProbe:
httpGet:
path: /api/health
port: 80
initialDelaySeconds: 5
periodSeconds: 30
failureThreshold: 12
timeoutSeconds: 60

postgresql:
enabled: false

Expand All @@ -187,7 +235,7 @@ redis:

minio:
enabled: false
defaultBuckets: 'lifecycle-logs'
defaultBuckets: 'lifecycle-logs,lifecycle-sites'
auth:
rootUser: minioadmin
rootPassword: minioadmin
Expand Down
87 changes: 87 additions & 0 deletions src/app/api/v2/sites/[siteId]/content/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Copyright 2026 GoodRx, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { NextRequest } from 'next/server';
import { createApiHandler } from 'server/lib/createApiHandler';
import { getRequestUserIdentity } from 'server/lib/get-user';
import { successResponse } from 'server/lib/response';
import { readUploadFile, sitesErrorResponse } from 'server/lib/sites/routeHelpers';
import SitesService from 'server/services/sites';

export const runtime = 'nodejs';

type RouteContext = {
params: {
siteId: string;
};
};

/**
* @openapi
* /api/v2/sites/{siteId}/content:
* put:
* summary: Replace hosted static site content
* description: Uploads a new static file or ZIP archive and makes it the active content for the site.
* tags:
* - Sites
* operationId: replaceSiteContent
* parameters:
* - in: path
* name: siteId
* required: true
* schema:
* type: string
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* $ref: '#/components/schemas/SiteUploadRequest'
* responses:
* '200':
* description: Hosted static site content replaced.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SiteSuccessResponse'
* '400':
* description: Invalid upload.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiErrorResponse'
* '404':
* description: Site not found or sites hosting is disabled.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiErrorResponse'
*/
const putHandler = async (req: NextRequest, { params }: RouteContext) => {
try {
const upload = await readUploadFile(req);
const service = new SitesService();
const site = await service.replaceSiteContent(params.siteId, {
...upload,
user: getRequestUserIdentity(req),
});
return successResponse({ site }, { status: 200 }, req);
} catch (error) {
return sitesErrorResponse(error, req);
}
};

export const PUT = createApiHandler(putHandler);
73 changes: 73 additions & 0 deletions src/app/api/v2/sites/[siteId]/extend/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Copyright 2026 GoodRx, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { NextRequest } from 'next/server';
import { createApiHandler } from 'server/lib/createApiHandler';
import { successResponse } from 'server/lib/response';
import { sitesErrorResponse } from 'server/lib/sites/routeHelpers';
import SitesService from 'server/services/sites';

type RouteContext = {
params: {
siteId: string;
};
};

/**
* @openapi
* /api/v2/sites/{siteId}/extend:
* post:
* summary: Extend a hosted static site's expiration
* tags:
* - Sites
* operationId: extendSite
* parameters:
* - in: path
* name: siteId
* required: true
* schema:
* type: string
* responses:
* '200':
* description: Hosted static site expiration extended.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SiteSuccessResponse'
* '400':
* description: TTL is disabled for hosted sites.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiErrorResponse'
* '404':
* description: Site not found or sites hosting is disabled.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiErrorResponse'
*/
const postHandler = async (req: NextRequest, { params }: RouteContext) => {
try {
const service = new SitesService();
const site = await service.extendSite(params.siteId);
return successResponse({ site }, { status: 200 }, req);
} catch (error) {
return sitesErrorResponse(error, req);
}
};

export const POST = createApiHandler(postHandler);
102 changes: 102 additions & 0 deletions src/app/api/v2/sites/[siteId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Copyright 2026 GoodRx, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { NextRequest } from 'next/server';
import { createApiHandler } from 'server/lib/createApiHandler';
import { successResponse } from 'server/lib/response';
import { sitesErrorResponse } from 'server/lib/sites/routeHelpers';
import SitesService from 'server/services/sites';

type RouteContext = {
params: {
siteId: string;
};
};

/**
* @openapi
* /api/v2/sites/{siteId}:
* get:
* summary: Get a hosted static site
* tags:
* - Sites
* operationId: getSite
* parameters:
* - in: path
* name: siteId
* required: true
* schema:
* type: string
* responses:
* '200':
* description: Hosted static site.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SiteSuccessResponse'
* '404':
* description: Site not found or sites hosting is disabled.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiErrorResponse'
* delete:
* summary: Delete a hosted static site
* tags:
* - Sites
* operationId: deleteSite
* parameters:
* - in: path
* name: siteId
* required: true
* schema:
* type: string
* responses:
* '200':
* description: Hosted static site deleted.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SiteSuccessResponse'
* '404':
* description: Site not found or sites hosting is disabled.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiErrorResponse'
*/
const getHandler = async (req: NextRequest, { params }: RouteContext) => {
try {
const service = new SitesService();
const site = await service.getSite(params.siteId);
return successResponse({ site }, { status: 200 }, req);
} catch (error) {
return sitesErrorResponse(error, req);
}
};

const deleteHandler = async (req: NextRequest, { params }: RouteContext) => {
try {
const service = new SitesService();
const site = await service.deleteSite(params.siteId);
return successResponse({ site }, { status: 200 }, req);
} catch (error) {
return sitesErrorResponse(error, req);
}
};

export const GET = createApiHandler(getHandler);
export const DELETE = createApiHandler(deleteHandler);
Loading
Loading