Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard,cli,docs): implement limits #390

Merged
merged 3 commits into from Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/happy-teachers-relax.md
@@ -0,0 +1,6 @@
---
'@lagon/cli': patch
'@lagon/dashboard': patch
---

Implement limits
5 changes: 5 additions & 0 deletions .changeset/late-dogs-sparkle.md
@@ -0,0 +1,5 @@
---
'@lagon/docs': patch
---

Add limits page
4 changes: 2 additions & 2 deletions packages/cli/src/commands/build.rs
Expand Up @@ -23,7 +23,7 @@ pub fn build(file: PathBuf, client: Option<PathBuf>, public_dir: Option<PathBuf>
let end_progress = print_progress("Writting index.js...");

fs::create_dir_all(".lagon")?;
fs::write(".lagon/index.js", index.get_ref())?;
fs::write(".lagon/index.js", index)?;

end_progress();

Expand All @@ -35,7 +35,7 @@ pub fn build(file: PathBuf, client: Option<PathBuf>, public_dir: Option<PathBuf>
.join("public")
.join(PathBuf::from(&path).parent().unwrap());
fs::create_dir_all(dir)?;
fs::write(format!(".lagon/public/{}", path), content.get_ref())?;
fs::write(format!(".lagon/public/{}", path), content)?;

end_progress();
}
Expand Down
9 changes: 4 additions & 5 deletions packages/cli/src/commands/dev.rs
Expand Up @@ -21,8 +21,7 @@ use std::time::Duration;
use tokio::sync::Mutex;

use crate::utils::{
bundle_function, info, input, success, validate_code_file, validate_public_dir, warn,
FileCursor,
bundle_function, info, input, success, validate_code_file, validate_public_dir, warn, Assets,
};

use log::{
Expand Down Expand Up @@ -76,7 +75,7 @@ fn parse_environment_variables(env: Option<PathBuf>) -> Result<HashMap<String, S
async fn handle_request(
req: HyperRequest<Body>,
ip: String,
content: Arc<Mutex<(FileCursor, HashMap<String, FileCursor>)>>,
content: Arc<Mutex<(Vec<u8>, Assets)>>,
environment_variables: HashMap<String, String>,
) -> Result<HyperResponse<Body>> {
let mut url = req.uri().to_string();
Expand Down Expand Up @@ -119,7 +118,7 @@ async fn handle_request(
let response = Response {
status: 200,
headers: Some(headers),
body: Bytes::from(asset.1.get_ref().to_vec()),
body: Bytes::from(asset.1.clone()),
};

tx.send_async(RunResult::Response(response))
Expand All @@ -131,7 +130,7 @@ async fn handle_request(
request.add_header("X-Forwarded-For".into(), ip);

let mut isolate = Isolate::new(
IsolateOptions::new(String::from_utf8(index.get_ref().to_vec())?)
IsolateOptions::new(String::from_utf8(index)?)
.with_metadata(Some((String::from(""), String::from(""))))
.with_environment_variables(environment_variables),
);
Expand Down
10 changes: 5 additions & 5 deletions packages/cli/src/commands/undeploy.rs
Expand Up @@ -10,13 +10,13 @@ use crate::utils::{

#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct DeleteDeploymentRequest {
struct UndeployDeploymentRequest {
function_id: String,
deployment_id: String,
}

#[derive(Deserialize, Debug)]
struct DeleteDeploymentResponse {
struct UndeployDeploymentResponse {
#[allow(dead_code)]
ok: bool,
}
Expand All @@ -41,9 +41,9 @@ pub async fn undeploy(file: PathBuf, deployment_id: String) -> Result<()> {
true => {
let end_progress = print_progress("Deleting Deployment...");
TrpcClient::new(&config)
.mutation::<DeleteDeploymentRequest, DeleteDeploymentResponse>(
"deploymentDelete",
DeleteDeploymentRequest {
.mutation::<UndeployDeploymentRequest, UndeployDeploymentResponse>(
"deploymentUndeploy",
UndeployDeploymentRequest {
function_id: function_config.function_id,
deployment_id,
},
Expand Down
67 changes: 53 additions & 14 deletions packages/cli/src/utils/deployments.rs
Expand Up @@ -4,20 +4,19 @@ use hyper::{Body, Method, Request};
use std::{
collections::HashMap,
fs,
io::Cursor,
path::{Path, PathBuf},
process::Command,
};
use walkdir::WalkDir;
use walkdir::{DirEntry, WalkDir};

use pathdiff::diff_paths;
use serde::{Deserialize, Serialize};

use crate::utils::{debug, print_progress, success, TrpcClient};

use super::Config;
use super::{Config, MAX_ASSETS_PER_FUNCTION, MAX_ASSET_SIZE_MB, MAX_FUNCTION_SIZE_MB};

pub type FileCursor = Cursor<Vec<u8>>;
pub type Assets = HashMap<String, Vec<u8>>;

#[derive(Serialize, Deserialize, Debug)]
pub struct DeploymentConfig {
Expand Down Expand Up @@ -73,7 +72,7 @@ pub fn delete_function_config(file: &Path) -> Result<()> {
Ok(())
}

fn esbuild(file: &PathBuf) -> Result<FileCursor> {
fn esbuild(file: &PathBuf) -> Result<Vec<u8>> {
let result = Command::new("esbuild")
.arg(file)
.arg("--bundle")
Expand All @@ -87,7 +86,14 @@ fn esbuild(file: &PathBuf) -> Result<FileCursor> {
if result.status.success() {
let output = result.stdout;

return Ok(Cursor::new(output));
if output.len() >= MAX_FUNCTION_SIZE_MB {
return Err(anyhow!(
"Function can't be larger than {} bytes",
MAX_FUNCTION_SIZE_MB
));
}

return Ok(output);
}

Err(anyhow!(
Expand All @@ -101,7 +107,7 @@ pub fn bundle_function(
index: &PathBuf,
client: &Option<PathBuf>,
public_dir: &PathBuf,
) -> Result<(FileCursor, HashMap<String, FileCursor>)> {
) -> Result<(Vec<u8>, Assets)> {
if Command::new("esbuild").arg("--version").output().is_err() {
return Err(anyhow!(
"esbuild is not installed. Please install it with `npm install -g esbuild`",
Expand All @@ -112,7 +118,7 @@ pub fn bundle_function(
let index_output = esbuild(index)?;
end_progress();

let mut assets = HashMap::<String, FileCursor>::new();
let mut assets = Assets::new();

if let Some(client) = client {
let end_progress = print_progress("Bundling client file...");
Expand All @@ -139,19 +145,38 @@ pub fn bundle_function(
);
let end_progress = print_progress(&msg);

for file in WalkDir::new(public_dir) {
let files = WalkDir::new(public_dir)
.into_iter()
.collect::<Vec<walkdir::Result<DirEntry>>>();

if files.len() >= MAX_ASSETS_PER_FUNCTION {
return Err(anyhow!(
"Too many assets in public directory, max is {}",
MAX_ASSETS_PER_FUNCTION
));
}

for file in files {
let file = file?;
let path = file.path();

if path.is_file() {
if path.metadata()?.len() >= MAX_ASSET_SIZE_MB {
return Err(anyhow!(
"File {:?} can't be larger than {} bytes",
path,
MAX_ASSET_SIZE_MB
));
}

let diff = diff_paths(path, public_dir)
.unwrap()
.to_str()
.unwrap()
.to_string();
let file_content = fs::read(path)?;

assets.insert(diff, Cursor::new(file_content));
assets.insert(diff, file_content);
}
}

Expand All @@ -163,11 +188,18 @@ pub fn bundle_function(
Ok((index_output, assets))
}

#[derive(Serialize, Debug)]
struct Asset {
name: String,
size: usize,
}

#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct CreateDeploymentRequest {
function_id: String,
assets: Vec<String>,
function_size: usize,
assets: Vec<Asset>,
}

#[derive(Deserialize, Debug)]
Expand Down Expand Up @@ -209,7 +241,14 @@ pub async fn create_deployment(
"deploymentCreate",
CreateDeploymentRequest {
function_id: function_id.clone(),
assets: assets.keys().cloned().collect(),
function_size: index.len(),
assets: assets
.iter()
.map(|(key, value)| Asset {
name: key.clone(),
size: value.len(),
})
.collect(),
},
)
.await?;
Expand All @@ -227,7 +266,7 @@ pub async fn create_deployment(
let request = Request::builder()
.method(Method::PUT)
.uri(code_url)
.body(Body::from(index.into_inner()))?;
.body(Body::from(index))?;

trpc_client.client.request(request).await?;

Expand All @@ -240,7 +279,7 @@ pub async fn create_deployment(
let request = Request::builder()
.method(Method::PUT)
.uri(url)
.body(Body::from(asset.clone().into_inner()))?;
.body(Body::from(asset.clone()))?;

trpc_client.client.request(request).await?;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/utils/mod.rs
Expand Up @@ -11,6 +11,10 @@ pub use console::*;
pub use deployments::*;
pub use trpc::*;

pub const MAX_FUNCTION_SIZE_MB: usize = 10 * 1024 * 1024; // 10MB
pub const MAX_ASSET_SIZE_MB: u64 = 10 * 1024 * 1024; // 10MB
pub const MAX_ASSETS_PER_FUNCTION: usize = 100;

pub fn validate_code_file(file: &Path) -> Result<()> {
if !file.exists() || !file.is_file() {
return Err(anyhow!("{} is not a file", file.to_str().unwrap()));
Expand Down
26 changes: 22 additions & 4 deletions packages/dashboard/lib/constants.ts
@@ -1,13 +1,27 @@
export const ORGANIZATION_NAME_MIN_LENGTH = 3;
export const ORGANIZATION_NAME_MAX_LENGTH = 20;
export const ORGANIZATION_DESCRIPTION_MAX_LENGTH = 200;
export const ORGANIZATION_NAME_MAX_LENGTH = 64;
export const ORGANIZATION_DESCRIPTION_MAX_LENGTH = 256;

export const FUNCTION_NAME_MIN_LENGTH = 5;
export const FUNCTION_NAME_MAX_LENGTH = 20;
export const FUNCTION_NAME_MIN_LENGTH = 3;
export const FUNCTION_NAME_MAX_LENGTH = 64;

export const FUNCTION_DEFAULT_MEMORY = 128; // 128MB
export const FUNCTION_DEFAULT_TIMEOUT = 50; // 50ms
export const FUNCTION_DEFAULT_STARTUP_TIMEOUT = 200; // 200ms
export const MAX_FUNCTIONS_PER_ORGANIZATION = 20;

export const MAX_FUNCTION_SIZE_MB = 10 * 1024 * 1024; // 10MB
export const MAX_ASSET_SIZE_MB = 10 * 1024 * 1024; // 10MB
export const MAX_ASSETS_PER_FUNCTION = 100;

export const ENVIRONMENT_VARIABLE_KEY_MAX_LENGTH = 64;
export const ENVIRONMENT_VARIABLE_VALUE_MAX_SIZE = 5 * 1024; // 5KB
export const ENVIRONMENT_VARIABLES_PER_FUNCTION = 100;

export const CUSTOM_DOMAINS_PER_FUNCTION = 10;

export const PRESIGNED_URL_EXPIRES_SECONDS = 60 * 60; // 1 hour

export const REGIONS = {
'ashburn-us-east': 'Ashburn (us-east)',
'hillsboro-us-west': 'Hillsboro (us-west)',
Expand All @@ -22,3 +36,7 @@ export const REGIONS = {
};

export type Regions = keyof typeof REGIONS;

export const DEFAULT_FUNCTION = `export function handler(request) {
return new Response("Hello World!")
}`;
17 changes: 10 additions & 7 deletions packages/dashboard/lib/pages/function/FunctionDeployments.tsx
Expand Up @@ -16,20 +16,20 @@ type FunctionDeploymentsProps = {
const FunctionDeployments = ({ func, refetch }: FunctionDeploymentsProps) => {
const { scopedT } = useI18n();
const t = scopedT('functions.deployments');
const deleteDeployment = trpc.deploymentDelete.useMutation();
const undeployDeployment = trpc.deploymentUndeploy.useMutation();
const promoteDeployment = trpc.deploymentPromote.useMutation();

const removeDeplomyent = useCallback(
async (deployment: { id: string }) => {
await deleteDeployment.mutateAsync({
await undeployDeployment.mutateAsync({
functionId: func?.id || '',
deploymentId: deployment.id,
});

await refetch();
toast.success(t('delete.success'));
},
[func?.id, deleteDeployment, refetch, t],
[func?.id, undeployDeployment, refetch, t],
);

const promoteDeploymentHandler = useCallback(
Expand Down Expand Up @@ -106,7 +106,10 @@ const FunctionDeployments = ({ func, refetch }: FunctionDeploymentsProps) => {
title={t('promote.modal.title')}
description={t('promote.modal.description')}
disclosure={
<Button leftIcon={<ArrowPathIcon className="w-4 h-4" />} disabled={deleteDeployment.isLoading}>
<Button
leftIcon={<ArrowPathIcon className="w-4 h-4" />}
disabled={undeployDeployment.isLoading}
>
{t('promote')}
</Button>
}
Expand All @@ -122,17 +125,17 @@ const FunctionDeployments = ({ func, refetch }: FunctionDeploymentsProps) => {
title={t('delete.modal.title')}
description={t('delete.modal.description')}
disclosure={
<Button variant="danger" disabled={deleteDeployment.isLoading}>
<Button variant="danger" disabled={undeployDeployment.isLoading}>
{t('delete')}
</Button>
}
>
<Dialog.Buttons>
<Dialog.Cancel disabled={deleteDeployment.isLoading} />
<Dialog.Cancel disabled={undeployDeployment.isLoading} />
<Dialog.Action
variant="danger"
onClick={() => removeDeplomyent(deployment)}
disabled={deleteDeployment.isLoading}
disabled={undeployDeployment.isLoading}
>
{t('delete.modal.submit')}
</Dialog.Action>
Expand Down