mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 16:05:43 +00:00
fix: add support for GCS object storage (#6083)
* AI: Updates to files (run 15923369101) * all * ee ref * unneeded --------- Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> Co-authored-by: HugoCasa <hugo@casademont.ch>
This commit is contained in:
co-authored by
windmill-internal-app[bot]
HugoCasa
parent
8ba3959ada
commit
c51e128920
Generated
+1
@@ -8574,6 +8574,7 @@ dependencies = [
|
||||
"rand 0.9.0",
|
||||
"reqwest 0.12.20",
|
||||
"ring 0.17.14",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
|
||||
+1
-1
@@ -348,7 +348,7 @@ nkeys = "0.4.4"
|
||||
nu-parser = { version = "0.101.0", default-features = false }
|
||||
|
||||
datafusion = "47.0.0"
|
||||
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure"] }
|
||||
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure", "gcp"] }
|
||||
openidconnect = { version = "4.0.0-rc.1" }
|
||||
aws-config = "^1"
|
||||
aws-sdk-sqs = "1.57.0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
651b945d4567081968005278d7e87ea41cabbe1c
|
||||
b7c6fc065a3da98933d50fae697784da61c3fe2d
|
||||
|
||||
@@ -16563,11 +16563,13 @@ components:
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: ["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc"]
|
||||
enum: ["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc", "GoogleCloudStorage"]
|
||||
s3_resource_path:
|
||||
type: string
|
||||
azure_blob_resource_path:
|
||||
type: string
|
||||
gcs_resource_path:
|
||||
type: string
|
||||
public_resource:
|
||||
type: boolean
|
||||
secondary_storage:
|
||||
@@ -16578,11 +16580,13 @@ components:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc"]
|
||||
["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc", "GoogleCloudStorage"]
|
||||
s3_resource_path:
|
||||
type: string
|
||||
azure_blob_resource_path:
|
||||
type: string
|
||||
gcs_resource_path:
|
||||
type: string
|
||||
public_resource:
|
||||
type: boolean
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ use object_store::aws::AwsCredential;
|
||||
#[cfg(feature = "parquet")]
|
||||
use object_store::azure::MicrosoftAzureBuilder;
|
||||
#[cfg(feature = "parquet")]
|
||||
use object_store::gcp::GoogleCloudStorageBuilder;
|
||||
#[cfg(feature = "parquet")]
|
||||
use object_store::ObjectStore;
|
||||
#[cfg(feature = "parquet")]
|
||||
use object_store::{aws::AmazonS3Builder, ClientOptions};
|
||||
@@ -221,6 +223,7 @@ pub enum LargeFileStorage {
|
||||
AzureBlobStorage(AzureBlobStorage),
|
||||
S3AwsOidc(S3Storage),
|
||||
AzureWorkloadIdentity(AzureBlobStorage),
|
||||
GoogleCloudStorage(GoogleCloudStorage),
|
||||
// TODO: Add a filesystem type here in the future if needed
|
||||
}
|
||||
|
||||
@@ -238,10 +241,18 @@ pub struct AzureBlobStorage {
|
||||
pub public_resource: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GoogleCloudStorage {
|
||||
pub gcs_resource_path: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub public_resource: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ObjectStoreResource {
|
||||
S3(S3Resource),
|
||||
Azure(AzureBlobResource),
|
||||
Gcs(GcsResource),
|
||||
}
|
||||
|
||||
impl ObjectStoreResource {
|
||||
@@ -259,6 +270,7 @@ pub enum StorageResourceType {
|
||||
AzureBlob,
|
||||
S3AwsOidc,
|
||||
AzureWorkloadIdentity,
|
||||
GoogleCloudStorage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
@@ -300,6 +312,22 @@ pub struct AzureBlobResource {
|
||||
pub federated_token_file: Option<String>,
|
||||
}
|
||||
|
||||
fn as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
let v: serde_json::Value = Deserialize::deserialize(deserializer)?;
|
||||
serde_json::to_string(&v).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct GcsResource {
|
||||
pub bucket: String,
|
||||
#[serde(rename = "serviceAccountKey")]
|
||||
#[serde(deserialize_with = "as_string")]
|
||||
pub service_account_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Hash)]
|
||||
pub struct S3AwsOidcResource {
|
||||
#[serde(rename = "bucket")]
|
||||
@@ -380,6 +408,7 @@ pub async fn build_object_store_client(
|
||||
ObjectStoreResource::Azure(azure_blob_resource_ref) => {
|
||||
build_azure_blob_client(&azure_blob_resource_ref)
|
||||
}
|
||||
ObjectStoreResource::Gcs(gcs_resource_ref) => build_gcs_client(&gcs_resource_ref).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,18 +604,59 @@ fn build_azure_blob_client(
|
||||
return Ok(Arc::new(store));
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
async fn build_gcs_client(gcs_resource_ref: &GcsResource) -> error::Result<Arc<dyn ObjectStore>> {
|
||||
let gcs_resource = gcs_resource_ref.clone();
|
||||
|
||||
let mut store_builder = GoogleCloudStorageBuilder::new()
|
||||
.with_client_options(
|
||||
ClientOptions::new()
|
||||
.with_timeout_disabled()
|
||||
.with_default_headers(HeaderMap::from_iter(vec![(
|
||||
"Accept-Encoding".parse().unwrap(),
|
||||
"".parse().unwrap(),
|
||||
)])),
|
||||
)
|
||||
.with_bucket_name(gcs_resource.bucket);
|
||||
|
||||
store_builder = store_builder.with_service_account_key(gcs_resource.service_account_key);
|
||||
|
||||
// if private key is malformed, it will panic => https://github.com/apache/arrow-rs-object-store/issues/419
|
||||
let store = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| store_builder.build()))
|
||||
.map_err(|panic_info| {
|
||||
tracing::error!(
|
||||
"Panic while building GCS object store client: {:?}",
|
||||
panic_info
|
||||
);
|
||||
error::Error::internal_err(format!(
|
||||
"Panic while building GCS object store client: {:?}",
|
||||
panic_info
|
||||
))
|
||||
})?
|
||||
.map_err(|err| {
|
||||
tracing::error!("Error building GCS object store client: {:?}", err);
|
||||
error::Error::internal_err(format!(
|
||||
"Error building GCS object store client: {}",
|
||||
err.to_string()
|
||||
))
|
||||
})?;
|
||||
|
||||
return Ok(Arc::new(store));
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "typ", content = "value")]
|
||||
pub enum ObjectStoreSettings {
|
||||
S3(S3Settings),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ObjectSettings {
|
||||
S3(S3Settings),
|
||||
Azure(AzureBlobResource),
|
||||
AwsOidc(S3AwsOidcResource),
|
||||
Gcs(GcsResource),
|
||||
}
|
||||
|
||||
impl ObjectSettings {
|
||||
@@ -595,6 +665,7 @@ impl ObjectSettings {
|
||||
ObjectSettings::S3(s3_settings) => s3_settings.bucket.as_ref(),
|
||||
ObjectSettings::Azure(azure_settings) => Some(&azure_settings.container_name),
|
||||
ObjectSettings::AwsOidc(s3_aws_oidc_settings) => Some(&s3_aws_oidc_settings.bucket),
|
||||
ObjectSettings::Gcs(gcs_settings) => Some(&gcs_settings.bucket),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -628,6 +699,12 @@ pub async fn build_object_store_from_settings(
|
||||
refresh: Some(ObjectStoreRefresh::new(settings.clone(), res.expiration())),
|
||||
})
|
||||
}
|
||||
ObjectSettings::Gcs(gcs_settings) => {
|
||||
let gcs_resource = gcs_settings;
|
||||
build_gcs_client(&gcs_resource)
|
||||
.await
|
||||
.map(|x| ExpirableObjectStore::from(x))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -775,6 +775,13 @@ async fn get_workspace_s3_resource_path(
|
||||
resource_path.to_string(),
|
||||
)
|
||||
}
|
||||
Some(LargeFileStorage::GoogleCloudStorage(gcs)) => {
|
||||
let resource_path = gcs.gcs_resource_path.trim_start_matches("$res:");
|
||||
(
|
||||
StorageResourceType::GoogleCloudStorage,
|
||||
resource_path.to_string(),
|
||||
)
|
||||
}
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="md"
|
||||
size="xs"
|
||||
btnClasses="mt-1"
|
||||
on:click={() => {
|
||||
if ($values[setting.key] == undefined || !Array.isArray($values[setting.key])) {
|
||||
@@ -668,7 +668,7 @@
|
||||
disabled={!$enterpriseLicense}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="md"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
try {
|
||||
await SettingService.testCriticalChannels({
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import TestConnection from './TestConnection.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
|
||||
type S3Config = {
|
||||
type: 'S3'
|
||||
@@ -35,7 +36,14 @@
|
||||
roleArn: string
|
||||
}
|
||||
|
||||
export let bucket_config: S3Config | AzureConfig | AwsOidcConfig | undefined = undefined
|
||||
type GcsConfig = {
|
||||
type: 'Gcs'
|
||||
bucket: string
|
||||
serviceAccountKey: Record<string, string>
|
||||
}
|
||||
|
||||
export let bucket_config: S3Config | AzureConfig | AwsOidcConfig | GcsConfig | undefined =
|
||||
undefined
|
||||
|
||||
$: bucket_config?.type == 'S3' &&
|
||||
bucket_config.allow_http == undefined &&
|
||||
@@ -107,9 +115,9 @@
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
bind:selected={bucket_config.type}
|
||||
selected={bucket_config?.type ?? 'S3'}
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'S3') {
|
||||
if (e.detail === 'S3' && bucket_config?.type !== 'S3') {
|
||||
bucket_config = {
|
||||
type: 'S3',
|
||||
bucket: '',
|
||||
@@ -118,7 +126,7 @@
|
||||
secret_key: '',
|
||||
endpoint: ''
|
||||
}
|
||||
} else if (e.detail === 'Azure') {
|
||||
} else if (e.detail === 'Azure' && bucket_config?.type !== 'Azure') {
|
||||
bucket_config = {
|
||||
type: 'Azure',
|
||||
accountName: '',
|
||||
@@ -128,12 +136,26 @@
|
||||
clientId: '',
|
||||
accessKey: ''
|
||||
}
|
||||
} else if (e.detail === 'Gcs' && bucket_config?.type !== 'Gcs') {
|
||||
bucket_config = {
|
||||
type: 'Gcs',
|
||||
bucket: '',
|
||||
serviceAccountKey: {}
|
||||
}
|
||||
} else if (e.detail === 'AwsOidc' && bucket_config?.type !== 'AwsOidc') {
|
||||
bucket_config = {
|
||||
type: 'AwsOidc',
|
||||
bucket: '',
|
||||
region: '',
|
||||
roleArn: ''
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab size="sm" value="S3">S3</Tab>
|
||||
<Tab size="sm" value="Azure">Azure Blob</Tab>
|
||||
<Tab size="sm" value="AwsOidc">AWS OIDC</Tab>
|
||||
<Tab size="sm" value="Gcs">Google Cloud Storage</Tab>
|
||||
</Tabs>
|
||||
<div class="flex flex-col gap-2 mt-2 p-2 border rounded-md">
|
||||
{#if bucket_config.type === 'S3'}
|
||||
@@ -236,6 +258,37 @@
|
||||
bind:value={bucket_config.roleArn}
|
||||
/>
|
||||
</label>
|
||||
{:else if bucket_config.type === 'Gcs'}
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Bucket</span>
|
||||
<input type="text" placeholder="bucket-name" bind:value={bucket_config.bucket} />
|
||||
</label>
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Service Account Key</span>
|
||||
<span class="text-tertiary text-2xs">JSON content of the service account key file</span>
|
||||
<SimpleEditor
|
||||
lang="json"
|
||||
bind:code={
|
||||
() => {
|
||||
if (bucket_config?.type === 'Gcs') {
|
||||
return JSON.stringify(bucket_config.serviceAccountKey)
|
||||
} else {
|
||||
return '{}'
|
||||
}
|
||||
},
|
||||
(v) => {
|
||||
if (bucket_config?.type === 'Gcs') {
|
||||
try {
|
||||
bucket_config.serviceAccountKey = JSON.parse(v ?? '{}')
|
||||
} catch (_) {
|
||||
bucket_config.serviceAccountKey = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
class="h-80"
|
||||
/>
|
||||
</label>
|
||||
{:else}
|
||||
<div>Unknown bucket type {bucket_config['type']}</div>
|
||||
{/if}
|
||||
|
||||
@@ -39,12 +39,15 @@
|
||||
|
||||
<div class="flex flex-col gap-4 my-8">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-primary text-lg font-semibold">Workspace Object Storage (S3/Azure Blob)</div>
|
||||
<div class="text-primary text-lg font-semibold"
|
||||
>Workspace Object Storage (S3/Azure Blob/GCS)</div
|
||||
>
|
||||
<Description
|
||||
link="https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill#workspace-object-storage"
|
||||
>
|
||||
Connect your Windmill workspace to your S3 bucket or your Azure Blob storage to enable users
|
||||
to read and write from S3 without having to have access to the credentials.
|
||||
Connect your Windmill workspace to your S3 bucket, Azure Blob storage, or Google Cloud Storage
|
||||
to enable users to read and write from object storage without having to have access to the
|
||||
credentials.
|
||||
</Description>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,6 +77,7 @@
|
||||
<Tab size="xs" value="azure_blob">Azure Blob</Tab>
|
||||
<Tab exact size="xs" value="s3_aws_oidc">AWS OIDC</Tab>
|
||||
<Tab size="xs" value="azure_workload_identity">Azure Workload Identity</Tab>
|
||||
<Tab exact size="xs" value="gcloud_storage">Google Cloud Storage</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div class="w-full flex gap-1 mt-4">
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { GetSettingsResponse, LargeFileStorage } from './gen'
|
||||
import { emptyString } from './utils'
|
||||
|
||||
type s3type = 's3' | 'azure_blob' | 's3_aws_oidc' | 'azure_workload_identity'
|
||||
// Extended type to include GCS support until backend types are regenerated
|
||||
|
||||
type s3type = 's3' | 'azure_blob' | 's3_aws_oidc' | 'azure_workload_identity' | 'gcloud_storage'
|
||||
type s3ResourceSettingsItem = {
|
||||
resourceType: s3type
|
||||
resourcePath: string | undefined
|
||||
@@ -49,6 +51,13 @@ export function convertBackendSettingsToFrontendSettingsItem(
|
||||
resourcePath: large_file_storage?.s3_resource_path?.replace('$res:', ''),
|
||||
publicResource: large_file_storage?.public_resource
|
||||
}
|
||||
} else if (large_file_storage?.type === 'GoogleCloudStorage') {
|
||||
const gcsStorage = large_file_storage
|
||||
return {
|
||||
resourceType: 'gcloud_storage',
|
||||
resourcePath: gcsStorage?.gcs_resource_path?.replace('$res:', ''),
|
||||
publicResource: gcsStorage?.public_resource
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
resourceType: 's3',
|
||||
@@ -91,6 +100,10 @@ export function convertFrontendToBackendettingsItem(
|
||||
let typ: LargeFileStorage['type'] = 'S3AwsOidc'
|
||||
params['type'] = typ
|
||||
params['s3_resource_path'] = resourcePathWithPrefix
|
||||
} else if (s3ResourceSettings.resourceType === 'gcloud_storage') {
|
||||
let typ: LargeFileStorage['type'] = 'GoogleCloudStorage'
|
||||
params['type'] = typ
|
||||
params['gcs_resource_path'] = resourcePathWithPrefix
|
||||
} else {
|
||||
let typ: LargeFileStorage['type'] = 'S3Storage'
|
||||
params['type'] = typ
|
||||
|
||||
Reference in New Issue
Block a user