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

registry: allow publishing extensions of several Postgres versions #572

Merged
merged 9 commits into from
Dec 28, 2023
Merged
78 changes: 74 additions & 4 deletions registry/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ utoipa-swagger-ui = { version = "4.0.0", features = ["actix-web"] }
tar = "0.4.40"
flate2 = "1.0.28"
bytes = "1.5.0"
sha256 = "1.4.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ALTER TABLE v1.extension_dependency
ADD CONSTRAINT unique_dependency UNIQUE (extension_version_id, depends_on_extension_name);

ALTER TABLE v1.extension_configurations
ADD CONSTRAINT unique_extension_config UNIQUE (extension_version_id, configuration_name);

ALTER TABLE v1.trunk_project_postgres_support
ADD CONSTRAINT unique_trunk_project_postgres_support UNIQUE (postgres_version_id, trunk_project_version_id);

ALTER TABLE v1.extensions_loadable_libraries
ADD CONSTRAINT unique_extension_library UNIQUE (extension_version_id, library_name);

ALTER TABLE v1.control_file
ADD CONSTRAINT control_file_extension_version_id_unique UNIQUE (extension_version_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE v1.trunk_project_downloads
ADD CONSTRAINT unique_downloads UNIQUE (platform_id, postgres_version_id, trunk_project_version_id, download_url);
122 changes: 75 additions & 47 deletions registry/sqlx-data.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions registry/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,7 @@ pub enum ExtensionRegistryError {

#[error("Not a GitHub repository: {0}")]
InvalidGithubRepo(String),

#[error("Failed to decompress Trunk archive")]
ArchiveError,
}
46 changes: 35 additions & 11 deletions registry/src/routes/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use crate::readme::GithubApiClient;
use crate::repository::Registry;
use crate::token::validate_token;
use crate::uploader::upload_extension;
use crate::v1::repository::TrunkProjectView;
use crate::v1::extractor;
use crate::v1::repository::{Download, ExtensionView, TrunkProjectView};
use crate::views::extension_publish::ExtensionUpload;
use crate::views::user_info::UserInfo;
use actix_multipart::Multipart;
Expand Down Expand Up @@ -290,15 +291,25 @@ pub async fn publish(
// The uploaded contents in .tar.gz
let gzipped_archive = file.freeze();

let digest = sha256::digest(&*gzipped_archive);

// Extract the .tar.gz and its relevant contentss
let (extension_views, pg_version) =
extractor::extract_extension_view(&gzipped_archive, &new_extension).map_err(|err| {
tracing::error!("Failed to decompress archive: {err}");
ExtensionRegistryError::ArchiveError
})?;

// TODO(ianstanton) Generate checksum
let file_byte_stream = ByteStream::from(gzipped_archive.clone());
let client = aws_sdk_s3::Client::new(&aws_config);
upload_extension(
let uploaded_path = upload_extension(
&cfg.bucket_name,
&client,
file_byte_stream,
&new_extension,
&new_extension.vers,
pg_version,
)
.await?;

Expand All @@ -315,29 +326,42 @@ pub async fn publish(
.await
.map_err(|err| error!("Failed to fetch README in /publish: {err}"));

let _ = insert_into_v1(new_extension, registry.as_ref(), gzipped_archive.as_ref())
.await
.map_err(|err| error!("Failed to insert extension into v1 in /publish: {err}"));
// Insert into v1 tables
let _ = insert_into_v1(
new_extension,
extension_views,
pg_version,
uploaded_path,
digest,
registry.as_ref(),
)
.await
.map_err(|err| error!("Failed to insert extension into v1 in /publish: {err}"));

Ok(response)
}

async fn insert_into_v1(
new_extension: ExtensionUpload,
extension_views: Vec<ExtensionView>,
pg_version: u8,
uploaded_path: String,
digest: String,
registry: &Registry,
gzipped_archive: &[u8],
) -> anyhow::Result<()> {
let extension_views =
crate::v1::extractor::extract_extension_view(gzipped_archive, &new_extension)?;

let trunk_project = TrunkProjectView {
name: new_extension.name,
description: new_extension.description.unwrap_or_default(),
documentation_link: new_extension.documentation,
repository_link: new_extension.repository.unwrap_or_default(),
version: new_extension.vers.to_string(),
// TODO: state in Trunk.toml the supported versions
postgres_versions: None,
postgres_versions: Some(vec![pg_version]),
downloads: Some(vec![Download {
link: uploaded_path,
pg_version,
architecture: "linux/amd64".into(),
sha256: digest,
}]),
extensions: extension_views,
};

Expand Down
25 changes: 18 additions & 7 deletions registry/src/uploader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,23 @@ use tracing::{debug, info};
const CACHE_CONTROL_IMMUTABLE: &str = "public,max-age=31536000,immutable";

/// Returns the internal path of an uploaded extension's version archive.
fn extension_path(name: &str, version: &str) -> String {
format!("extensions/{name}/{name}-{version}.tar.gz")
fn extension_path(name: &str, extension_version: &str, pg_version: u8) -> String {
format!("extensions/{name}/{name}-pg{pg_version}-{extension_version}.tar.gz")
}

/// Returns the URL of an uploaded extension's version archive.
///
/// The function doesn't check for the existence of the file.
pub fn extension_location(bucket_name: &str, project_name: &str, version: &str) -> String {
pub fn extension_location(
bucket_name: &str,
project_name: &str,
extension_version: &str,
) -> String {
// Note(vini): the current download endpoint only supports Postgres 15
let pg_version = 15;

let host = format!("{bucket_name}.s3.amazonaws.com");
let path = extension_path(project_name, version);
let path = extension_path(project_name, extension_version, pg_version);
format!("https://{host}/{path}")
}

Expand All @@ -46,15 +53,19 @@ pub async fn upload(
}

/// Uploads an extension file.
///
/// Returns the path of the uploaded archive.
pub async fn upload_extension(
bucket_name: &str,
s3_client: &aws_sdk_s3::Client,
file: ByteStream,
extension: &ExtensionUpload,
vers: &semver::Version,
extension_version: &semver::Version,
pg_version: u8,
) -> Result<String, ExtensionRegistryError> {
let path = extension_path(&extension.name, &vers.to_string());
let path = extension_path(&extension.name, &extension_version.to_string(), pg_version);
info!("Uploading extension: {:?}", extension);
upload(bucket_name, s3_client, &path, file, "application/gzip").await?;
Ok("Successfully uploaded extension".to_owned())

Ok(path)
}
Loading
Loading