diff --git a/.golangci.yaml b/.golangci.yaml index ca3f9dd44..ddec61b7e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -95,6 +95,8 @@ linters-settings: pkg: github.com/arangodb/kube-arangodb/integrations/storage/v2/shared - alias: pbImplStorageV2SharedGCS pkg: github.com/arangodb/kube-arangodb/integrations/storage/v2/shared/gcs + - alias: pbImplStorageV2SharedAzureBlobStorage + pkg: github.com/arangodb/kube-arangodb/integrations/storage/v2/shared/abs - alias: pbImplStorageV2SharedS3 pkg: github.com/arangodb/kube-arangodb/integrations/storage/v2/shared/s3 - alias: pbStorageV2 diff --git a/CHANGELOG.md b/CHANGELOG.md index fdc197988..c50fcab6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Change Log ## [master](https://github.com/arangodb/kube-arangodb/tree/master) (N/A) +- (Feature) Add scrape annotations for ArangoD pods +- (Feature) (Platform) Azure Storage Integration ## [1.3.3](https://github.com/arangodb/kube-arangodb/tree/1.3.3) (2025-12-02) - (Bugfix) (Platform) Fix Container Resource Adjustments @@ -9,7 +11,6 @@ - (Feature) (Platform) Dump CLI switch to Services - (Feature) (Platform) Fix ImagePullSecrets Merge - (Feature) (Platform) Update Failed Releases -- (Feature) Add scrape annotations for ArangoD pods ## [1.3.2](https://github.com/arangodb/kube-arangodb/tree/1.3.2) (2025-11-20) - (Bugfix) (Platform) Increase memory limit for Inventory diff --git a/docs/api/ArangoPlatformStorage.V1Beta1.md b/docs/api/ArangoPlatformStorage.V1Beta1.md index 40241e3fa..90cade7b6 100644 --- a/docs/api/ArangoPlatformStorage.V1Beta1.md +++ b/docs/api/ArangoPlatformStorage.V1Beta1.md @@ -8,6 +8,63 @@ title: ArangoPlatformStorage V1Beta1 ## Spec +### .spec.backend.azureBlobStorage.accountName + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.3/pkg/apis/platform/v1beta1/storage_spec_backend_abs.go#L39) + +This field is **required** + +AccountName specifies the Azure Storage AccountName +used in format https://.blob.core.windows.net/ + +*** + +### .spec.backend.azureBlobStorage.bucketName + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.3/pkg/apis/platform/v1beta1/storage_spec_backend_abs.go#L46) + +This field is **required** + +BucketName specifies the name of the bucket + +*** + +### .spec.backend.azureBlobStorage.bucketPath + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.3/pkg/apis/platform/v1beta1/storage_spec_backend_abs.go#L50) + +BucketPath specifies the Prefix within the bucket + +*** + +### .spec.backend.azureBlobStorage.credentialsSecret.name + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.3/pkg/apis/shared/v1/object.go#L53) + +This field is **required** + +Name of the object + +*** + +### .spec.backend.azureBlobStorage.endpoint + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.3/pkg/apis/platform/v1beta1/storage_spec_backend_abs.go#L42) + +Endpoint specifies the Azure Storage custom endpoint + +*** + +### .spec.backend.azureBlobStorage.tenantID + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.3/pkg/apis/platform/v1beta1/storage_spec_backend_abs.go#L34) + +This field is **required** + +TenantID specifies the Azure TenantID + +*** + ### .spec.backend.gcs.bucketName Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.3/pkg/apis/platform/v1beta1/storage_spec_backend_gcs.go#L35) diff --git a/docs/cli/arangodb_operator_integration.md b/docs/cli/arangodb_operator_integration.md index db5492d9d..dcd272bbc 100644 --- a/docs/cli/arangodb_operator_integration.md +++ b/docs/cli/arangodb_operator_integration.md @@ -18,109 +18,119 @@ Available Commands: help Help about any command Flags: - --database.endpoint string Endpoint of ArangoDB (Env: DATABASE_ENDPOINT) (default "localhost") - --database.port int Port of ArangoDB (Env: DATABASE_PORT) (default 8529) - --database.proto string Proto of the ArangoDB endpoint (Env: DATABASE_PROTO) (default "http") - --database.rf int ArangoDB ReplicationFactor (Env: DATABASE_RF) (default 1) - --database.wc int ArangoDB WriteConcern (Env: DATABASE_WC) (default 1) - --health.address string Address to expose health service (Env: HEALTH_ADDRESS) (default "0.0.0.0:9091") - --health.auth.token string Token for health service (when auth service is token) (Env: HEALTH_AUTH_TOKEN) - --health.auth.type string Auth type for health service (Env: HEALTH_AUTH_TYPE) (default "None") - --health.shutdown.enabled Determines if shutdown service should be enabled and exposed (Env: HEALTH_SHUTDOWN_ENABLED) (default true) - --health.tls.keyfile string Path to the keyfile (Env: HEALTH_TLS_KEYFILE) - -h, --help help for arangodb_operator_integration - --integration.authentication.v1 Enable AuthenticationV1 Integration Service (Env: INTEGRATION_AUTHENTICATION_V1) - --integration.authentication.v1.enabled Defines if Authentication is enabled (Env: INTEGRATION_AUTHENTICATION_V1_ENABLED) (default true) - --integration.authentication.v1.external Defines if External access to service authentication.v1 is enabled (Env: INTEGRATION_AUTHENTICATION_V1_EXTERNAL) - --integration.authentication.v1.internal Defines if Internal access to service authentication.v1 is enabled (Env: INTEGRATION_AUTHENTICATION_V1_INTERNAL) (default true) - --integration.authentication.v1.path string Path to the JWT Folder (Env: INTEGRATION_AUTHENTICATION_V1_PATH) - --integration.authentication.v1.token.allowed strings Allowed users for the Token (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_ALLOWED) - --integration.authentication.v1.token.max-size uint16 Max Token max size in bytes (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_MAX_SIZE) (default 64) - --integration.authentication.v1.token.ttl.default duration Default Token TTL (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_TTL_DEFAULT) (default 1h0m0s) - --integration.authentication.v1.token.ttl.max duration Max Token TTL (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_TTL_MAX) (default 1h0m0s) - --integration.authentication.v1.token.ttl.min duration Min Token TTL (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_TTL_MIN) (default 1m0s) - --integration.authentication.v1.token.user string Default user of the Token (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_USER) (default "root") - --integration.authentication.v1.ttl duration TTL of the JWT cache (Env: INTEGRATION_AUTHENTICATION_V1_TTL) (default 15s) - --integration.authorization.v0 Enable AuthorizationV0 Integration Service (Env: INTEGRATION_AUTHORIZATION_V0) - --integration.authorization.v0.external Defines if External access to service authorization.v0 is enabled (Env: INTEGRATION_AUTHORIZATION_V0_EXTERNAL) - --integration.authorization.v0.internal Defines if Internal access to service authorization.v0 is enabled (Env: INTEGRATION_AUTHORIZATION_V0_INTERNAL) (default true) - --integration.config.v1 Enable ConfigV1 Integration Service (Env: INTEGRATION_CONFIG_V1) - --integration.config.v1.external Defines if External access to service config.v1 is enabled (Env: INTEGRATION_CONFIG_V1_EXTERNAL) - --integration.config.v1.internal Defines if Internal access to service config.v1 is enabled (Env: INTEGRATION_CONFIG_V1_INTERNAL) (default true) - --integration.config.v1.module strings Module in the reference = (Env: INTEGRATION_CONFIG_V1_MODULE) - --integration.envoy.auth.v3 Enable EnvoyAuthV3 Integration Service (Env: INTEGRATION_ENVOY_AUTH_V3) - --integration.envoy.auth.v3.auth.enabled Defines if SSO Auth extension is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_AUTH_ENABLED) - --integration.envoy.auth.v3.auth.path string Path of the config file (Env: INTEGRATION_ENVOY_AUTH_V3_AUTH_PATH) - --integration.envoy.auth.v3.auth.type string Defines type of the authentication (Env: INTEGRATION_ENVOY_AUTH_V3_AUTH_TYPE) (default "OpenID") - --integration.envoy.auth.v3.enabled Defines if Auth extension is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_ENABLED) (default true) - --integration.envoy.auth.v3.extensions.cookie.jwt Defines if Cookie JWT extension is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_EXTENSIONS_COOKIE_JWT) (default true) - --integration.envoy.auth.v3.extensions.jwt Defines if JWT extension is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_EXTENSIONS_JWT) (default true) - --integration.envoy.auth.v3.extensions.users.create Defines if UserCreation extension is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_EXTENSIONS_USERS_CREATE) - --integration.envoy.auth.v3.external Defines if External access to service envoy.auth.v3 is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_EXTERNAL) - --integration.envoy.auth.v3.internal Defines if Internal access to service envoy.auth.v3 is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_INTERNAL) (default true) - --integration.events.v1 Enable EventsV1 Integration Service (Env: INTEGRATION_EVENTS_V1) - --integration.events.v1.async Enables async injection of the events (Env: INTEGRATION_EVENTS_V1_ASYNC) (default true) - --integration.events.v1.async.retry.delay duration Delay of the retries (Env: INTEGRATION_EVENTS_V1_ASYNC_RETRY_DELAY) (default 1s) - --integration.events.v1.async.retry.timeout duration Timeout for the event injection (Env: INTEGRATION_EVENTS_V1_ASYNC_RETRY_TIMEOUT) (default 1m0s) - --integration.events.v1.async.size int Size of the async queue (Env: INTEGRATION_EVENTS_V1_ASYNC_SIZE) (default 16) - --integration.events.v1.external Defines if External access to service events.v1 is enabled (Env: INTEGRATION_EVENTS_V1_EXTERNAL) - --integration.events.v1.internal Defines if Internal access to service events.v1 is enabled (Env: INTEGRATION_EVENTS_V1_INTERNAL) (default true) - --integration.meta.v1 Enable MetaV1 Integration Service (Env: INTEGRATION_META_V1) - --integration.meta.v1.external Defines if External access to service meta.v1 is enabled (Env: INTEGRATION_META_V1_EXTERNAL) - --integration.meta.v1.internal Defines if Internal access to service meta.v1 is enabled (Env: INTEGRATION_META_V1_INTERNAL) (default true) - --integration.meta.v1.prefix string Meta Key Prefix (Env: INTEGRATION_META_V1_PREFIX) - --integration.meta.v1.ttl duration Cache Object TTL (Env: INTEGRATION_META_V1_TTL) - --integration.scheduler.v1 SchedulerV1 Integration (Env: INTEGRATION_SCHEDULER_V1) - --integration.scheduler.v1.external Defines if External access to service scheduler.v1 is enabled (Env: INTEGRATION_SCHEDULER_V1_EXTERNAL) - --integration.scheduler.v1.internal Defines if Internal access to service scheduler.v1 is enabled (Env: INTEGRATION_SCHEDULER_V1_INTERNAL) (default true) - --integration.scheduler.v1.namespace string Kubernetes Namespace (Env: INTEGRATION_SCHEDULER_V1_NAMESPACE) (default "default") - --integration.scheduler.v1.verify-access Verify the CRD Access (Env: INTEGRATION_SCHEDULER_V1_VERIFY_ACCESS) (default true) - --integration.scheduler.v2 SchedulerV2 Integration (Env: INTEGRATION_SCHEDULER_V2) - --integration.scheduler.v2.deployment string ArangoDeployment Name (Env: INTEGRATION_SCHEDULER_V2_DEPLOYMENT) - --integration.scheduler.v2.driver string Helm Driver (Env: INTEGRATION_SCHEDULER_V2_DRIVER) (default "secret") - --integration.scheduler.v2.external Defines if External access to service scheduler.v2 is enabled (Env: INTEGRATION_SCHEDULER_V2_EXTERNAL) - --integration.scheduler.v2.internal Defines if Internal access to service scheduler.v2 is enabled (Env: INTEGRATION_SCHEDULER_V2_INTERNAL) (default true) - --integration.scheduler.v2.namespace string Kubernetes Namespace (Env: INTEGRATION_SCHEDULER_V2_NAMESPACE) (default "default") - --integration.shutdown.v1 ShutdownV1 Handler (Env: INTEGRATION_SHUTDOWN_V1) - --integration.shutdown.v1.debug.enabled Defines if debug extension is enabled (Env: INTEGRATION_SHUTDOWN_V1_DEBUG_ENABLED) - --integration.shutdown.v1.debug.path string Path of the Debug Directory (Env: INTEGRATION_SHUTDOWN_V1_DEBUG_PATH) (default "/debug") - --integration.shutdown.v1.debug.timeout duration Timeout of the Debug action (Env: INTEGRATION_SHUTDOWN_V1_DEBUG_TIMEOUT) (default 1m0s) - --integration.shutdown.v1.external Defines if External access to service shutdown.v1 is enabled (Env: INTEGRATION_SHUTDOWN_V1_EXTERNAL) - --integration.shutdown.v1.internal Defines if Internal access to service shutdown.v1 is enabled (Env: INTEGRATION_SHUTDOWN_V1_INTERNAL) (default true) - --integration.storage.v2 StorageBucket V2 Integration (Env: INTEGRATION_STORAGE_V2) - --integration.storage.v2.external Defines if External access to service storage.v2 is enabled (Env: INTEGRATION_STORAGE_V2_EXTERNAL) - --integration.storage.v2.gcs.bucket.name string Bucket name (Env: INTEGRATION_STORAGE_V2_GCS_BUCKET_NAME) - --integration.storage.v2.gcs.bucket.prefix string Bucket Prefix (Env: INTEGRATION_STORAGE_V2_GCS_BUCKET_PREFIX) - --integration.storage.v2.gcs.project-id string GCP Project ID (Env: INTEGRATION_STORAGE_V2_GCS_PROJECT_ID) - --integration.storage.v2.gcs.provider.sa.file string Path to the file with ServiceAccount JSON (Env: INTEGRATION_STORAGE_V2_GCS_PROVIDER_SA_FILE) - --integration.storage.v2.gcs.provider.sa.json string ServiceAccount JSON (Env: INTEGRATION_STORAGE_V2_GCS_PROVIDER_SA_JSON) - --integration.storage.v2.gcs.provider.type string Type of the provided credentials (Env: INTEGRATION_STORAGE_V2_GCS_PROVIDER_TYPE) (default "serviceAccount") - --integration.storage.v2.internal Defines if Internal access to service storage.v2 is enabled (Env: INTEGRATION_STORAGE_V2_INTERNAL) (default true) - --integration.storage.v2.s3.allow-insecure If set to true, the Endpoint certificates won't be checked (Env: INTEGRATION_STORAGE_V2_S3_ALLOW_INSECURE) - --integration.storage.v2.s3.bucket.name string Bucket name (Env: INTEGRATION_STORAGE_V2_S3_BUCKET_NAME) - --integration.storage.v2.s3.bucket.prefix string Bucket Prefix (Env: INTEGRATION_STORAGE_V2_S3_BUCKET_PREFIX) - --integration.storage.v2.s3.ca strings Path to file containing CA certificate to validate endpoint connection (Env: INTEGRATION_STORAGE_V2_S3_CA) - --integration.storage.v2.s3.disable-ssl If set to true, the SSL won't be used when connecting to Endpoint (Env: INTEGRATION_STORAGE_V2_S3_DISABLE_SSL) - --integration.storage.v2.s3.endpoint string Endpoint of S3 API implementation (Env: INTEGRATION_STORAGE_V2_S3_ENDPOINT) - --integration.storage.v2.s3.provider.file.access-key string Path to file containing S3 AccessKey (Env: INTEGRATION_STORAGE_V2_S3_PROVIDER_FILE_ACCESS_KEY) - --integration.storage.v2.s3.provider.file.secret-key string Path to file containing S3 SecretKey (Env: INTEGRATION_STORAGE_V2_S3_PROVIDER_FILE_SECRET_KEY) - --integration.storage.v2.s3.provider.type string S3 Credentials Provider type (Env: INTEGRATION_STORAGE_V2_S3_PROVIDER_TYPE) (default "file") - --integration.storage.v2.s3.region string Region (Env: INTEGRATION_STORAGE_V2_S3_REGION) - --integration.storage.v2.type string Type of the Storage Integration (Env: INTEGRATION_STORAGE_V2_TYPE) (default "s3") - --services.address string Address to expose internal services (Env: SERVICES_ADDRESS) (default "127.0.0.1:9092") - --services.auth.token string Token for internal service (when auth service is token) (Env: SERVICES_AUTH_TOKEN) - --services.auth.type string Auth type for internal service (Env: SERVICES_AUTH_TYPE) (default "None") - --services.enabled Defines if internal access is enabled (Env: SERVICES_ENABLED) (default true) - --services.external.address string Address to expose external services (Env: SERVICES_EXTERNAL_ADDRESS) (default "0.0.0.0:9093") - --services.external.auth.token string Token for external service (when auth service is token) (Env: SERVICES_EXTERNAL_AUTH_TOKEN) - --services.external.auth.type string Auth type for external service (Env: SERVICES_EXTERNAL_AUTH_TYPE) (default "None") - --services.external.enabled Defines if external access is enabled (Env: SERVICES_EXTERNAL_ENABLED) - --services.external.gateway.address string Address to expose external gateway services (Env: SERVICES_EXTERNAL_GATEWAY_ADDRESS) (default "0.0.0.0:9193") - --services.external.gateway.enabled Defines if external gateway is enabled (Env: SERVICES_EXTERNAL_GATEWAY_ENABLED) - --services.external.tls.keyfile string Path to the keyfile (Env: SERVICES_EXTERNAL_TLS_KEYFILE) - --services.gateway.address string Address to expose internal gateway services (Env: SERVICES_GATEWAY_ADDRESS) (default "127.0.0.1:9192") - --services.gateway.enabled Defines if internal gateway is enabled (Env: SERVICES_GATEWAY_ENABLED) (default true) - --services.tls.keyfile string Path to the keyfile (Env: SERVICES_TLS_KEYFILE) + --database.endpoint string Endpoint of ArangoDB (Env: DATABASE_ENDPOINT) (default "localhost") + --database.port int Port of ArangoDB (Env: DATABASE_PORT) (default 8529) + --database.proto string Proto of the ArangoDB endpoint (Env: DATABASE_PROTO) (default "http") + --database.rf int ArangoDB ReplicationFactor (Env: DATABASE_RF) (default 1) + --database.wc int ArangoDB WriteConcern (Env: DATABASE_WC) (default 1) + --health.address string Address to expose health service (Env: HEALTH_ADDRESS) (default "0.0.0.0:9091") + --health.auth.token string Token for health service (when auth service is token) (Env: HEALTH_AUTH_TOKEN) + --health.auth.type string Auth type for health service (Env: HEALTH_AUTH_TYPE) (default "None") + --health.shutdown.enabled Determines if shutdown service should be enabled and exposed (Env: HEALTH_SHUTDOWN_ENABLED) (default true) + --health.tls.keyfile string Path to the keyfile (Env: HEALTH_TLS_KEYFILE) + -h, --help help for arangodb_operator_integration + --integration.authentication.v1 Enable AuthenticationV1 Integration Service (Env: INTEGRATION_AUTHENTICATION_V1) + --integration.authentication.v1.enabled Defines if Authentication is enabled (Env: INTEGRATION_AUTHENTICATION_V1_ENABLED) (default true) + --integration.authentication.v1.external Defines if External access to service authentication.v1 is enabled (Env: INTEGRATION_AUTHENTICATION_V1_EXTERNAL) + --integration.authentication.v1.internal Defines if Internal access to service authentication.v1 is enabled (Env: INTEGRATION_AUTHENTICATION_V1_INTERNAL) (default true) + --integration.authentication.v1.path string Path to the JWT Folder (Env: INTEGRATION_AUTHENTICATION_V1_PATH) + --integration.authentication.v1.token.allowed strings Allowed users for the Token (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_ALLOWED) + --integration.authentication.v1.token.max-size uint16 Max Token max size in bytes (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_MAX_SIZE) (default 64) + --integration.authentication.v1.token.ttl.default duration Default Token TTL (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_TTL_DEFAULT) (default 1h0m0s) + --integration.authentication.v1.token.ttl.max duration Max Token TTL (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_TTL_MAX) (default 1h0m0s) + --integration.authentication.v1.token.ttl.min duration Min Token TTL (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_TTL_MIN) (default 1m0s) + --integration.authentication.v1.token.user string Default user of the Token (Env: INTEGRATION_AUTHENTICATION_V1_TOKEN_USER) (default "root") + --integration.authentication.v1.ttl duration TTL of the JWT cache (Env: INTEGRATION_AUTHENTICATION_V1_TTL) (default 15s) + --integration.authorization.v0 Enable AuthorizationV0 Integration Service (Env: INTEGRATION_AUTHORIZATION_V0) + --integration.authorization.v0.external Defines if External access to service authorization.v0 is enabled (Env: INTEGRATION_AUTHORIZATION_V0_EXTERNAL) + --integration.authorization.v0.internal Defines if Internal access to service authorization.v0 is enabled (Env: INTEGRATION_AUTHORIZATION_V0_INTERNAL) (default true) + --integration.config.v1 Enable ConfigV1 Integration Service (Env: INTEGRATION_CONFIG_V1) + --integration.config.v1.external Defines if External access to service config.v1 is enabled (Env: INTEGRATION_CONFIG_V1_EXTERNAL) + --integration.config.v1.internal Defines if Internal access to service config.v1 is enabled (Env: INTEGRATION_CONFIG_V1_INTERNAL) (default true) + --integration.config.v1.module strings Module in the reference = (Env: INTEGRATION_CONFIG_V1_MODULE) + --integration.envoy.auth.v3 Enable EnvoyAuthV3 Integration Service (Env: INTEGRATION_ENVOY_AUTH_V3) + --integration.envoy.auth.v3.auth.enabled Defines if SSO Auth extension is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_AUTH_ENABLED) + --integration.envoy.auth.v3.auth.path string Path of the config file (Env: INTEGRATION_ENVOY_AUTH_V3_AUTH_PATH) + --integration.envoy.auth.v3.auth.type string Defines type of the authentication (Env: INTEGRATION_ENVOY_AUTH_V3_AUTH_TYPE) (default "OpenID") + --integration.envoy.auth.v3.enabled Defines if Auth extension is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_ENABLED) (default true) + --integration.envoy.auth.v3.extensions.cookie.jwt Defines if Cookie JWT extension is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_EXTENSIONS_COOKIE_JWT) (default true) + --integration.envoy.auth.v3.extensions.jwt Defines if JWT extension is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_EXTENSIONS_JWT) (default true) + --integration.envoy.auth.v3.extensions.users.create Defines if UserCreation extension is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_EXTENSIONS_USERS_CREATE) + --integration.envoy.auth.v3.external Defines if External access to service envoy.auth.v3 is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_EXTERNAL) + --integration.envoy.auth.v3.internal Defines if Internal access to service envoy.auth.v3 is enabled (Env: INTEGRATION_ENVOY_AUTH_V3_INTERNAL) (default true) + --integration.events.v1 Enable EventsV1 Integration Service (Env: INTEGRATION_EVENTS_V1) + --integration.events.v1.async Enables async injection of the events (Env: INTEGRATION_EVENTS_V1_ASYNC) (default true) + --integration.events.v1.async.retry.delay duration Delay of the retries (Env: INTEGRATION_EVENTS_V1_ASYNC_RETRY_DELAY) (default 1s) + --integration.events.v1.async.retry.timeout duration Timeout for the event injection (Env: INTEGRATION_EVENTS_V1_ASYNC_RETRY_TIMEOUT) (default 1m0s) + --integration.events.v1.async.size int Size of the async queue (Env: INTEGRATION_EVENTS_V1_ASYNC_SIZE) (default 16) + --integration.events.v1.external Defines if External access to service events.v1 is enabled (Env: INTEGRATION_EVENTS_V1_EXTERNAL) + --integration.events.v1.internal Defines if Internal access to service events.v1 is enabled (Env: INTEGRATION_EVENTS_V1_INTERNAL) (default true) + --integration.meta.v1 Enable MetaV1 Integration Service (Env: INTEGRATION_META_V1) + --integration.meta.v1.external Defines if External access to service meta.v1 is enabled (Env: INTEGRATION_META_V1_EXTERNAL) + --integration.meta.v1.internal Defines if Internal access to service meta.v1 is enabled (Env: INTEGRATION_META_V1_INTERNAL) (default true) + --integration.meta.v1.prefix string Meta Key Prefix (Env: INTEGRATION_META_V1_PREFIX) + --integration.meta.v1.ttl duration Cache Object TTL (Env: INTEGRATION_META_V1_TTL) + --integration.scheduler.v1 SchedulerV1 Integration (Env: INTEGRATION_SCHEDULER_V1) + --integration.scheduler.v1.external Defines if External access to service scheduler.v1 is enabled (Env: INTEGRATION_SCHEDULER_V1_EXTERNAL) + --integration.scheduler.v1.internal Defines if Internal access to service scheduler.v1 is enabled (Env: INTEGRATION_SCHEDULER_V1_INTERNAL) (default true) + --integration.scheduler.v1.namespace string Kubernetes Namespace (Env: INTEGRATION_SCHEDULER_V1_NAMESPACE) (default "default") + --integration.scheduler.v1.verify-access Verify the CRD Access (Env: INTEGRATION_SCHEDULER_V1_VERIFY_ACCESS) (default true) + --integration.scheduler.v2 SchedulerV2 Integration (Env: INTEGRATION_SCHEDULER_V2) + --integration.scheduler.v2.deployment string ArangoDeployment Name (Env: INTEGRATION_SCHEDULER_V2_DEPLOYMENT) + --integration.scheduler.v2.driver string Helm Driver (Env: INTEGRATION_SCHEDULER_V2_DRIVER) (default "secret") + --integration.scheduler.v2.external Defines if External access to service scheduler.v2 is enabled (Env: INTEGRATION_SCHEDULER_V2_EXTERNAL) + --integration.scheduler.v2.internal Defines if Internal access to service scheduler.v2 is enabled (Env: INTEGRATION_SCHEDULER_V2_INTERNAL) (default true) + --integration.scheduler.v2.namespace string Kubernetes Namespace (Env: INTEGRATION_SCHEDULER_V2_NAMESPACE) (default "default") + --integration.shutdown.v1 ShutdownV1 Handler (Env: INTEGRATION_SHUTDOWN_V1) + --integration.shutdown.v1.debug.enabled Defines if debug extension is enabled (Env: INTEGRATION_SHUTDOWN_V1_DEBUG_ENABLED) + --integration.shutdown.v1.debug.path string Path of the Debug Directory (Env: INTEGRATION_SHUTDOWN_V1_DEBUG_PATH) (default "/debug") + --integration.shutdown.v1.debug.timeout duration Timeout of the Debug action (Env: INTEGRATION_SHUTDOWN_V1_DEBUG_TIMEOUT) (default 1m0s) + --integration.shutdown.v1.external Defines if External access to service shutdown.v1 is enabled (Env: INTEGRATION_SHUTDOWN_V1_EXTERNAL) + --integration.shutdown.v1.internal Defines if Internal access to service shutdown.v1 is enabled (Env: INTEGRATION_SHUTDOWN_V1_INTERNAL) (default true) + --integration.storage.v2 StorageBucket V2 Integration (Env: INTEGRATION_STORAGE_V2) + --integration.storage.v2.azure-blob-storage.account-name string AzureBlobStorage Account ID (Env: INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_ACCOUNT_NAME) + --integration.storage.v2.azure-blob-storage.bucket.name string Bucket name (Env: INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_BUCKET_NAME) + --integration.storage.v2.azure-blob-storage.bucket.prefix string Bucket Prefix (Env: INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_BUCKET_PREFIX) + --integration.storage.v2.azure-blob-storage.client.secret.client-id string Azure ClientID (Env: INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_CLIENT_SECRET_CLIENT_ID) + --integration.storage.v2.azure-blob-storage.client.secret.client-id-file string Azure ClientID File (Env: INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_CLIENT_SECRET_CLIENT_ID_FILE) + --integration.storage.v2.azure-blob-storage.client.secret.client-secret string Azure ClientSecret (Env: INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_CLIENT_SECRET_CLIENT_SECRET) + --integration.storage.v2.azure-blob-storage.client.secret.client-secret-file string Azure ClientSecret File (Env: INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_CLIENT_SECRET_CLIENT_SECRET_FILE) + --integration.storage.v2.azure-blob-storage.client.tenant-id string Azure Client Tenant ID (Env: INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_CLIENT_TENANT_ID) + --integration.storage.v2.azure-blob-storage.client.type string Azure Client Provider (Env: INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_CLIENT_TYPE) (default "secret") + --integration.storage.v2.azure-blob-storage.endpoint string AzureBlobStorage Endpoint (Env: INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_ENDPOINT) + --integration.storage.v2.external Defines if External access to service storage.v2 is enabled (Env: INTEGRATION_STORAGE_V2_EXTERNAL) + --integration.storage.v2.gcs.bucket.name string Bucket name (Env: INTEGRATION_STORAGE_V2_GCS_BUCKET_NAME) + --integration.storage.v2.gcs.bucket.prefix string Bucket Prefix (Env: INTEGRATION_STORAGE_V2_GCS_BUCKET_PREFIX) + --integration.storage.v2.gcs.project-id string GCP Project ID (Env: INTEGRATION_STORAGE_V2_GCS_PROJECT_ID) + --integration.storage.v2.gcs.provider.sa.file string Path to the file with ServiceAccount JSON (Env: INTEGRATION_STORAGE_V2_GCS_PROVIDER_SA_FILE) + --integration.storage.v2.gcs.provider.sa.json string ServiceAccount JSON (Env: INTEGRATION_STORAGE_V2_GCS_PROVIDER_SA_JSON) + --integration.storage.v2.gcs.provider.type string Type of the provided credentials (Env: INTEGRATION_STORAGE_V2_GCS_PROVIDER_TYPE) (default "serviceAccount") + --integration.storage.v2.internal Defines if Internal access to service storage.v2 is enabled (Env: INTEGRATION_STORAGE_V2_INTERNAL) (default true) + --integration.storage.v2.s3.allow-insecure If set to true, the Endpoint certificates won't be checked (Env: INTEGRATION_STORAGE_V2_S3_ALLOW_INSECURE) + --integration.storage.v2.s3.bucket.name string Bucket name (Env: INTEGRATION_STORAGE_V2_S3_BUCKET_NAME) + --integration.storage.v2.s3.bucket.prefix string Bucket Prefix (Env: INTEGRATION_STORAGE_V2_S3_BUCKET_PREFIX) + --integration.storage.v2.s3.ca strings Path to file containing CA certificate to validate endpoint connection (Env: INTEGRATION_STORAGE_V2_S3_CA) + --integration.storage.v2.s3.disable-ssl If set to true, the SSL won't be used when connecting to Endpoint (Env: INTEGRATION_STORAGE_V2_S3_DISABLE_SSL) + --integration.storage.v2.s3.endpoint string Endpoint of S3 API implementation (Env: INTEGRATION_STORAGE_V2_S3_ENDPOINT) + --integration.storage.v2.s3.provider.file.access-key string Path to file containing S3 AccessKey (Env: INTEGRATION_STORAGE_V2_S3_PROVIDER_FILE_ACCESS_KEY) + --integration.storage.v2.s3.provider.file.secret-key string Path to file containing S3 SecretKey (Env: INTEGRATION_STORAGE_V2_S3_PROVIDER_FILE_SECRET_KEY) + --integration.storage.v2.s3.provider.type string S3 Credentials Provider type (Env: INTEGRATION_STORAGE_V2_S3_PROVIDER_TYPE) (default "file") + --integration.storage.v2.s3.region string Region (Env: INTEGRATION_STORAGE_V2_S3_REGION) + --integration.storage.v2.type string Type of the Storage Integration (Env: INTEGRATION_STORAGE_V2_TYPE) (default "s3") + --services.address string Address to expose internal services (Env: SERVICES_ADDRESS) (default "127.0.0.1:9092") + --services.auth.token string Token for internal service (when auth service is token) (Env: SERVICES_AUTH_TOKEN) + --services.auth.type string Auth type for internal service (Env: SERVICES_AUTH_TYPE) (default "None") + --services.enabled Defines if internal access is enabled (Env: SERVICES_ENABLED) (default true) + --services.external.address string Address to expose external services (Env: SERVICES_EXTERNAL_ADDRESS) (default "0.0.0.0:9093") + --services.external.auth.token string Token for external service (when auth service is token) (Env: SERVICES_EXTERNAL_AUTH_TOKEN) + --services.external.auth.type string Auth type for external service (Env: SERVICES_EXTERNAL_AUTH_TYPE) (default "None") + --services.external.enabled Defines if external access is enabled (Env: SERVICES_EXTERNAL_ENABLED) + --services.external.gateway.address string Address to expose external gateway services (Env: SERVICES_EXTERNAL_GATEWAY_ADDRESS) (default "0.0.0.0:9193") + --services.external.gateway.enabled Defines if external gateway is enabled (Env: SERVICES_EXTERNAL_GATEWAY_ENABLED) + --services.external.tls.keyfile string Path to the keyfile (Env: SERVICES_EXTERNAL_TLS_KEYFILE) + --services.gateway.address string Address to expose internal gateway services (Env: SERVICES_GATEWAY_ADDRESS) (default "127.0.0.1:9192") + --services.gateway.enabled Defines if internal gateway is enabled (Env: SERVICES_GATEWAY_ENABLED) (default true) + --services.tls.keyfile string Path to the keyfile (Env: SERVICES_TLS_KEYFILE) Use "arangodb_operator_integration [command] --help" for more information about a command. ``` diff --git a/docs/platform/storage/azure_blob_storage.md b/docs/platform/storage/azure_blob_storage.md new file mode 100644 index 000000000..ae3185a4e --- /dev/null +++ b/docs/platform/storage/azure_blob_storage.md @@ -0,0 +1,43 @@ +--- +layout: page +title: Azure Blob Storage +parent: Storage +grand_parent: ArangoDBPlatform +nav_order: 3 +--- + +# Integration + +In order to connect to the Azure Blob storage: + +## Azure Credentials + +Client ID & Secret with access to the storage container and accounts need to be saved in the secret. + +```shell +kubectl create secret generic credentials --from-literal 'clientId=' --from-literal 'clientSecret=' +``` + +## Object + +Once the Secret is created, we are able to create ArangoPlatformStorage. + +``` +echo "--- +apiVersion: platform.arangodb.com/v1beta1 +kind: ArangoPlatformStorage +metadata: + name: deployment + namespace: namespace +spec: + backend: + azureBlobStorage: + bucketName: + bucketPath: + credentialsSecret: + name: credentials + tenantID: + accountName: + endpoint: +" | kubectl apply -f - +``` \ No newline at end of file diff --git a/docs/platform/storage/gcs.md b/docs/platform/storage/gcs.md index aefb1f9d0..3d4258e47 100644 --- a/docs/platform/storage/gcs.md +++ b/docs/platform/storage/gcs.md @@ -15,7 +15,7 @@ In order to connect to the GCS (Google Cloud Storage): ServiceAccount with access to the storage needs to be saved in the secret. ```shell -kubectl create secret generic ca --from-file 'serviceAccount=' +kubectl create secret generic credentials --from-file 'serviceAccount=' ``` ## Object @@ -36,6 +36,6 @@ spec: bucketPath: credentialsSecret: name: credentials - projectID: gcr-for-testing + projectID: " | kubectl apply -f - ``` \ No newline at end of file diff --git a/docs/platform/storage/minio.md b/docs/platform/storage/minio.md index b083cde1f..c9d401eab 100644 --- a/docs/platform/storage/minio.md +++ b/docs/platform/storage/minio.md @@ -3,7 +3,7 @@ layout: page title: MinIO parent: Storage grand_parent: ArangoDBPlatform -nav_order: 3 +nav_order: 4 --- # Integration diff --git a/go.mod b/go.mod index d92617ce7..4f42e4b62 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 golang.org/x/sync v0.17.0 golang.org/x/sys v0.38.0 golang.org/x/text v0.30.0 @@ -74,6 +74,9 @@ require ( require ( cloud.google.com/go/storage v1.55.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 github.com/Masterminds/semver/v3 v3.3.0 github.com/arangodb-managed/apis v0.89.1 github.com/arangodb-managed/integration-apis v0.2.1 @@ -81,7 +84,7 @@ require ( github.com/coreos/go-oidc/v3 v3.14.1 github.com/envoyproxy/go-control-plane/envoy v1.35.0 github.com/go-logr/zerologr v1.2.3 - github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 github.com/jedib0t/go-pretty/v6 v6.6.5 github.com/regclient/regclient v0.10.0 @@ -103,7 +106,9 @@ require ( cloud.google.com/go/monitoring v1.24.2 // indirect dario.cat/mergo v1.0.1 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect @@ -183,6 +188,7 @@ require ( github.com/kkdai/maglev v0.2.0 // indirect github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.2.4 // indirect @@ -209,6 +215,7 @@ require ( github.com/pavel-v-chernykh/keystore-go v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/common v0.55.0 // indirect diff --git a/go.sum b/go.sum index 97cefcf26..df5c36ee2 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,18 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -244,6 +254,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -342,6 +354,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= @@ -422,6 +436,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -595,6 +611,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= diff --git a/integrations/storage/v2/configuration.go b/integrations/storage/v2/configuration.go index 4378122c5..e67ef0010 100644 --- a/integrations/storage/v2/configuration.go +++ b/integrations/storage/v2/configuration.go @@ -24,6 +24,7 @@ import ( "context" pbImplStorageV2Shared "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared" + pbImplStorageV2SharedAzureBlobStorage "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared/abs" pbImplStorageV2SharedGCS "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared/gcs" pbImplStorageV2SharedS3 "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared/s3" "github.com/arangodb/kube-arangodb/pkg/util" @@ -33,8 +34,9 @@ import ( type ConfigurationType string const ( - ConfigurationTypeS3 ConfigurationType = "s3" - ConfigurationTypeGCS ConfigurationType = "gcs" + ConfigurationTypeS3 ConfigurationType = "s3" + ConfigurationTypeGCS ConfigurationType = "gcs" + ConfigurationTypeAzure ConfigurationType = "azureBlobStorage" ) func NewConfiguration(mods ...util.ModR[Configuration]) Configuration { @@ -46,8 +48,9 @@ func NewConfiguration(mods ...util.ModR[Configuration]) Configuration { type Configuration struct { Type ConfigurationType - S3 pbImplStorageV2SharedS3.Configuration - GCS pbImplStorageV2SharedGCS.Configuration + S3 pbImplStorageV2SharedS3.Configuration + GCS pbImplStorageV2SharedGCS.Configuration + AzureBlobStorage pbImplStorageV2SharedAzureBlobStorage.Configuration } func (c Configuration) IO(ctx context.Context) (pbImplStorageV2Shared.IO, error) { @@ -56,6 +59,8 @@ func (c Configuration) IO(ctx context.Context) (pbImplStorageV2Shared.IO, error) return c.S3.New() case ConfigurationTypeGCS: return c.GCS.New(ctx) + case ConfigurationTypeAzure: + return c.AzureBlobStorage.New() default: return nil, errors.Errorf("Unknown Type: %s", c.Type) } diff --git a/integrations/storage/v2/object.go b/integrations/storage/v2/object.go index 0860b7775..bbeee5cdc 100644 --- a/integrations/storage/v2/object.go +++ b/integrations/storage/v2/object.go @@ -27,10 +27,12 @@ import ( meta "k8s.io/apimachinery/pkg/apis/meta/v1" pbImplStorageV2Shared "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared" + pbImplStorageV2SharedAzureBlobStorage "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared/abs" pbImplStorageV2SharedGCS "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared/gcs" pbImplStorageV2SharedS3 "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared/s3" platformApi "github.com/arangodb/kube-arangodb/pkg/apis/platform/v1beta1" awsHelper "github.com/arangodb/kube-arangodb/pkg/util/aws" + "github.com/arangodb/kube-arangodb/pkg/util/azure" utilConstants "github.com/arangodb/kube-arangodb/pkg/util/constants" "github.com/arangodb/kube-arangodb/pkg/util/errors" gcsHelper "github.com/arangodb/kube-arangodb/pkg/util/gcs" @@ -138,6 +140,42 @@ func NewIOFromObject(ctx context.Context, client kclient.Client, in *platformApi return cfg.New(ctx) } + + if azureBlobStorage := backend.AzureBlobStorage; azureBlobStorage != nil { + var config azure.Config + + if v := azureBlobStorage.CredentialsSecret; v != nil { + secret, err := client.Kubernetes().CoreV1().Secrets(v.GetNamespace(in)).Get(ctx, v.GetName(), meta.GetOptions{}) + if err != nil { + return nil, errors.WithMessage(err, "Failed to get AzureBlobStorage secret") + } + + cid, ok := secret.Data[utilConstants.SecretCredentialsAzureBlobStorageClientID] + if !ok { + return nil, errors.Errorf("Failed to get AzureBlobStorage secret %s data: Key %s not found", secret.GetName(), utilConstants.SecretCredentialsAzureBlobStorageClientID) + } + + cs, ok := secret.Data[utilConstants.SecretCredentialsAzureBlobStorageClientSecret] + if !ok { + return nil, errors.Errorf("Failed to get AzureBlobStorage secret %s data: Key %s not found", secret.GetName(), utilConstants.SecretCredentialsAzureBlobStorageClientSecret) + } + + config.Provider.Secret.ClientID = string(cid) + config.Provider.Secret.ClientSecret = string(cs) + config.Provider.Type = azure.ProviderTypeSecret + } + + config.AccountName = azureBlobStorage.GetAccountName() + config.Provider.TenantID = azureBlobStorage.GetTenantID() + + var cfg pbImplStorageV2SharedAzureBlobStorage.Configuration + + cfg.BucketName = azureBlobStorage.GetBucketName() + cfg.BucketPrefix = azureBlobStorage.GetBucketPrefix() + cfg.Client = config + + return cfg.New() + } } return nil, errors.Errorf("Unable to init the storage") diff --git a/integrations/storage/v2/shared/abs/configuration.go b/integrations/storage/v2/shared/abs/configuration.go new file mode 100644 index 000000000..2d080d740 --- /dev/null +++ b/integrations/storage/v2/shared/abs/configuration.go @@ -0,0 +1,59 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package abs + +import ( + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" + + pbImplStorageV2Shared "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared" + "github.com/arangodb/kube-arangodb/pkg/util/azure" +) + +type Configuration struct { + BucketName string + BucketPrefix string + + MaxListKeys *int32 + + Client azure.Config +} + +func (c Configuration) New() (pbImplStorageV2Shared.IO, error) { + prov, err := c.Client.GetCredentials() + if err != nil { + return nil, err + } + + endpoint, err := c.Client.GetEndpoint() + if err != nil { + return nil, err + } + + client, err := azblob.NewClient(endpoint, prov, nil) + if err != nil { + return nil, err + } + + return &ios{ + config: c, + client: client.ServiceClient(), + }, nil +} diff --git a/integrations/storage/v2/shared/abs/delete.go b/integrations/storage/v2/shared/abs/delete.go new file mode 100644 index 000000000..e40683b4a --- /dev/null +++ b/integrations/storage/v2/shared/abs/delete.go @@ -0,0 +1,53 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package abs + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +func (i *ios) Delete(ctx context.Context, key string) (bool, error) { + q := i.container().NewBlockBlobClient(i.key(key)) + + _, err := q.GetProperties(ctx, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 404 { + return true, nil + } + return false, err + } + + _, err = q.Delete(ctx, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 404 { + return true, nil + } + return false, err + } + + return true, nil +} diff --git a/integrations/storage/v2/shared/abs/head.go b/integrations/storage/v2/shared/abs/head.go new file mode 100644 index 000000000..87b662f62 --- /dev/null +++ b/integrations/storage/v2/shared/abs/head.go @@ -0,0 +1,50 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package abs + +import ( + "context" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + + pbImplStorageV2Shared "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +func (i *ios) Head(ctx context.Context, key string) (*pbImplStorageV2Shared.Info, error) { + q := i.container().NewBlockBlobClient(i.key(key)) + + prop, err := q.GetProperties(ctx, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 404 { + return nil, nil + } + return nil, err + } + + return &pbImplStorageV2Shared.Info{ + Size: uint64(util.OptionalType(prop.ContentLength, 0)), + LastUpdatedAt: util.OptionalType(prop.LastModified, time.Time{}), + }, nil +} diff --git a/integrations/storage/v2/shared/abs/init.go b/integrations/storage/v2/shared/abs/init.go new file mode 100644 index 000000000..468942eba --- /dev/null +++ b/integrations/storage/v2/shared/abs/init.go @@ -0,0 +1,40 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package abs + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container" + + pbImplStorageV2Shared "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared" +) + +func (i *ios) Init(ctx context.Context, opts *pbImplStorageV2Shared.InitOptions) error { + c := i.container() + + _, err := c.GetProperties(ctx, &container.GetPropertiesOptions{}) + if err != nil { + return err + } + + return nil +} diff --git a/integrations/storage/v2/shared/abs/io.go b/integrations/storage/v2/shared/abs/io.go new file mode 100644 index 000000000..380044976 --- /dev/null +++ b/integrations/storage/v2/shared/abs/io.go @@ -0,0 +1,54 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package abs + +import ( + "path" + goStrings "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/service" +) + +type ios struct { + config Configuration + client *service.Client +} + +func (i *ios) container() *container.Client { + return i.client.NewContainerClient(i.config.BucketName) +} + +func (i *ios) clean(key string) string { + return goStrings.TrimPrefix(goStrings.TrimPrefix(key, i.key()), "/") +} + +func (i *ios) key(keys ...string) string { + out := path.Join(goStrings.TrimPrefix(i.config.BucketPrefix, "/"), path.Join(keys...)) + + if len(keys) > 0 { + if goStrings.HasSuffix(keys[len(keys)-1], "/") { + out = out + "/" + } + } + + return out +} diff --git a/integrations/storage/v2/shared/abs/io_test.go b/integrations/storage/v2/shared/abs/io_test.go new file mode 100644 index 000000000..c67518389 --- /dev/null +++ b/integrations/storage/v2/shared/abs/io_test.go @@ -0,0 +1,115 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package abs + +import ( + "fmt" + "io" + "os" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/uuid" + + pbImplStorageV2Shared "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared" + "github.com/arangodb/kube-arangodb/pkg/util/shutdown" + "github.com/arangodb/kube-arangodb/pkg/util/tests" +) + +func Test(t *testing.T) { + var config = Configuration{ + BucketName: tests.GetAzureBlobStorageContainer(t), + BucketPrefix: fmt.Sprintf("tmp/unit-test/%s/", uuid.NewUUID()), + MaxListKeys: nil, + Client: tests.GetAzureConfig(t), + } + + client, err := config.New() + require.NoError(t, err) + + require.NoError(t, client.Init(shutdown.Context(), &pbImplStorageV2Shared.InitOptions{})) + + // List Done + { + objs, err := client.List(shutdown.Context(), "") + require.NoError(t, err) + data, err := objs.Next(shutdown.Context()) + require.NoError(t, err) + require.Len(t, data, 0) + _, err = objs.Next(shutdown.Context()) + require.ErrorIs(t, err, io.EOF) + } + + { + // Write + data, err := client.Write(shutdown.Context(), "my-file.txt") + require.NoError(t, err) + + require.False(t, data.Closed()) + + _, err = data.Write([]byte("hello world")) + require.NoError(t, err) + + checksum, bytes, err := data.Close(shutdown.Context()) + require.NoError(t, err) + require.Equal(t, checksum, "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") + require.EqualValues(t, bytes, 11) + + require.True(t, data.Closed()) + } + + // List Done + { + objs, err := client.List(shutdown.Context(), "") + require.NoError(t, err) + data, err := objs.Next(shutdown.Context()) + require.NoError(t, err) + require.Len(t, data, 1) + _, err = objs.Next(shutdown.Context()) + require.ErrorIs(t, err, io.EOF) + } + + { + // Read + _, err := client.Read(shutdown.Context(), "my-file2.txt") + require.ErrorIs(t, err, os.ErrNotExist) + + } + + { + // Read + data, err := client.Read(shutdown.Context(), "my-file.txt") + require.NoError(t, err) + + require.False(t, data.Closed()) + + z, err := io.ReadAll(data) + require.NoError(t, err) + + checksum, bytes, err := data.Close(shutdown.Context()) + require.NoError(t, err) + require.Equal(t, checksum, "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") + require.EqualValues(t, bytes, 11) + require.Len(t, z, 11) + + require.True(t, data.Closed()) + } +} diff --git a/integrations/storage/v2/shared/abs/list.go b/integrations/storage/v2/shared/abs/list.go new file mode 100644 index 000000000..022f2f8d8 --- /dev/null +++ b/integrations/storage/v2/shared/abs/list.go @@ -0,0 +1,90 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package abs + +import ( + "context" + "io" + "sync" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container" + + pbImplStorageV2Shared "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +func (i *ios) List(ctx context.Context, key string) (util.NextIterator[[]pbImplStorageV2Shared.File], error) { + return &listIterator{ + pager: i.container().NewListBlobsFlatPager(&container.ListBlobsFlatOptions{ + Include: container.ListBlobsInclude{}, + Marker: nil, + MaxResults: i.config.MaxListKeys, + Prefix: util.NewType(i.key(key)), + }), + parent: i, + }, nil +} + +type listIterator struct { + lock sync.Mutex + parent *ios + + pager *runtime.Pager[container.ListBlobsFlatResponse] +} + +func (l *listIterator) Next(ctx context.Context) ([]pbImplStorageV2Shared.File, error) { + l.lock.Lock() + defer l.lock.Unlock() + + if !l.pager.More() { + return nil, io.EOF + } + + resp, err := l.pager.NextPage(ctx) + if err != nil { + return nil, err + } + + if resp.Segment == nil { + return nil, errors.Errorf("Invalid segment response") + } + + data := make([]pbImplStorageV2Shared.File, len(resp.Segment.BlobItems)) + + for id, file := range resp.Segment.BlobItems { + if file == nil || file.Properties == nil { + return nil, errors.Errorf("Invalid file response") + } + + data[id] = pbImplStorageV2Shared.File{ + Key: l.parent.clean(*file.Name), + Info: pbImplStorageV2Shared.Info{ + Size: uint64(util.OptionalType(file.Properties.ContentLength, 0)), + LastUpdatedAt: util.OptionalType(file.Properties.LastModified, time.Time{}), + }, + } + } + + return data, nil +} diff --git a/integrations/storage/v2/shared/abs/read.go b/integrations/storage/v2/shared/abs/read.go new file mode 100644 index 000000000..b84fff03a --- /dev/null +++ b/integrations/storage/v2/shared/abs/read.go @@ -0,0 +1,112 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package abs + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "hash" + "io" + "os" + "sync" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + + pbImplStorageV2Shared "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared" +) + +type reader struct { + lock sync.Mutex + + in io.ReadCloser + + bytes int64 + checksum hash.Hash + + closed bool +} + +func (r *reader) Read(p []byte) (n int, err error) { + r.lock.Lock() + defer r.lock.Unlock() + + n, err = r.in.Read(p) + if n > 0 { + r.bytes += int64(n) + r.checksum.Write(p[:n]) + } + + if err != nil { + if errors.Is(err, io.EOF) { + r.closed = true + if n > 0 { + return n, nil + } + return + } + + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 404 { + return 0, os.ErrNotExist + } + return 0, err + } + + return +} + +func (r *reader) Close(ctx context.Context) (string, int64, error) { + r.lock.Lock() + defer r.lock.Unlock() + + if err := r.in.Close(); err != nil { + return "", 0, err + } + + return fmt.Sprintf("%02x", r.checksum.Sum(nil)), r.bytes, nil +} + +func (r *reader) Closed() bool { + return r.closed +} + +func (i *ios) Read(ctx context.Context, key string) (pbImplStorageV2Shared.Reader, error) { + q := i.container().NewBlockBlobClient(i.key(key)) + + resp, err := q.DownloadStream(ctx, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 404 { + return nil, os.ErrNotExist + } + + return nil, err + } + + var reader reader + + reader.in = resp.Body + reader.checksum = sha256.New() + + return &reader, nil +} diff --git a/integrations/storage/v2/shared/abs/write.go b/integrations/storage/v2/shared/abs/write.go new file mode 100644 index 000000000..810425825 --- /dev/null +++ b/integrations/storage/v2/shared/abs/write.go @@ -0,0 +1,118 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package abs + +import ( + "context" + "crypto/sha256" + "fmt" + "hash" + "io" + "sync" + + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob" + + pbImplStorageV2Shared "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared" +) + +type writer struct { + lock sync.Mutex + + in io.WriteCloser + + done chan struct{} + + bytes int64 + checksum hash.Hash + + err error +} + +func (w *writer) Write(p []byte) (n int, err error) { + w.lock.Lock() + defer w.lock.Unlock() + + n, err = w.in.Write(p) + if err != nil { + return 0, err + } + + if n > 0 { + w.bytes += int64(n) + w.checksum.Write(p[:n]) + } + + return +} + +func (w *writer) Close(ctx context.Context) (string, int64, error) { + w.lock.Lock() + defer w.lock.Unlock() + + if err := w.in.Close(); err != nil { + return "", 0, err + } + + <-w.done + + if w.err != nil { + return "", 0, w.err + } + + return fmt.Sprintf("%02x", w.checksum.Sum(nil)), w.bytes, nil +} + +func (w *writer) Closed() bool { + w.lock.Lock() + defer w.lock.Unlock() + + select { + case <-w.done: + return true + default: + return false + } +} + +func (w *writer) run(ctx context.Context, client *blockblob.Client, data io.Reader) { + defer close(w.done) + + _, err := client.UploadStream(ctx, data, &blockblob.UploadStreamOptions{ + BlockSize: 4 * 1024 * 1024, + }) + w.err = err +} + +func (i *ios) Write(ctx context.Context, key string) (pbImplStorageV2Shared.Writer, error) { + q := i.container().NewBlockBlobClient(i.key(key)) + + in, out := io.Pipe() + + var writer writer + + writer.done = make(chan struct{}) + writer.in = out + writer.checksum = sha256.New() + + go writer.run(ctx, q, in) + + return &writer, nil +} diff --git a/integrations/storage/v2/suite_azure_test.go b/integrations/storage/v2/suite_azure_test.go new file mode 100644 index 000000000..fb40e113e --- /dev/null +++ b/integrations/storage/v2/suite_azure_test.go @@ -0,0 +1,113 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package v2 + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/uuid" + + pbImplStorageV2SharedAzureBlobStorage "github.com/arangodb/kube-arangodb/integrations/storage/v2/shared/abs" + platformApi "github.com/arangodb/kube-arangodb/pkg/apis/platform/v1beta1" + sharedApi "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" + "github.com/arangodb/kube-arangodb/pkg/util" + utilConstants "github.com/arangodb/kube-arangodb/pkg/util/constants" + "github.com/arangodb/kube-arangodb/pkg/util/kclient" + "github.com/arangodb/kube-arangodb/pkg/util/shutdown" + "github.com/arangodb/kube-arangodb/pkg/util/tests" +) + +func azureConfiguration(t *testing.T, mods ...util.ModR[Configuration]) Configuration { + var scfg pbImplStorageV2SharedAzureBlobStorage.Configuration + scfg.BucketName = tests.GetAzureBlobStorageContainer(t) + scfg.BucketPrefix = fmt.Sprintf("tmp/unit-test/%s/", uuid.NewUUID()) + scfg.MaxListKeys = nil + scfg.Client = tests.GetAzureConfig(t) + + var cfg Configuration + + cfg.Type = ConfigurationTypeAzure + cfg.AzureBlobStorage = scfg + + return cfg.With(mods...) +} + +func azureKubernetesObject(t *testing.T, mods ...util.Mod[platformApi.ArangoPlatformStorage]) (string, string, kclient.Client) { + client := kclient.NewFakeClient() + + config := tests.GetAzureConfig(t) + bucketName := tests.GetAzureBlobStorageContainer(t) + bucketPrefix := fmt.Sprintf("tmp/unit-test-object/%s/", uuid.NewUUID()) + + creds, err := client.Kubernetes().CoreV1().Secrets(tests.FakeNamespace).Create(shutdown.Context(), &core.Secret{ + ObjectMeta: meta.ObjectMeta{ + Name: "credentials", + Namespace: tests.FakeNamespace, + }, + Data: map[string][]byte{ + utilConstants.SecretCredentialsAzureBlobStorageClientID: []byte(config.Provider.Secret.ClientID), + utilConstants.SecretCredentialsAzureBlobStorageClientSecret: []byte(config.Provider.Secret.ClientSecret), + }, + }, meta.CreateOptions{}) + require.NoError(t, err) + + obj := &platformApi.ArangoPlatformStorage{ + ObjectMeta: meta.ObjectMeta{ + Name: "storage", + Namespace: tests.FakeNamespace, + }, + Spec: platformApi.ArangoPlatformStorageSpec{ + Backend: &platformApi.ArangoPlatformStorageSpecBackend{ + AzureBlobStorage: &platformApi.ArangoPlatformStorageSpecBackendAzureBlobStorage{ + TenantID: util.NewType(config.Provider.TenantID), + AccountName: util.NewType(config.AccountName), + BucketName: util.NewType(bucketName), + BucketPrefix: util.NewType(bucketPrefix), + CredentialsSecret: &sharedApi.Object{ + Name: creds.GetName(), + }, + }, + }, + }, + } + + util.ApplyMods(obj, mods...) + + obj, err = client.Arango().PlatformV1beta1().ArangoPlatformStorages(tests.FakeNamespace).Create(shutdown.Context(), obj, meta.CreateOptions{}) + require.NoError(t, err) + + return obj.GetName(), obj.GetNamespace(), client +} + +func Test_Azure_Handler(t *testing.T) { + testConfiguration(t, azureConfiguration, func(in Configuration) Configuration { + in.AzureBlobStorage.MaxListKeys = util.NewType[int32](32) + return in + }) +} + +func Test_Azure_Object(t *testing.T) { + testObject(t, azureKubernetesObject) +} diff --git a/pkg/apis/platform/v1beta1/storage_spec_backend.go b/pkg/apis/platform/v1beta1/storage_spec_backend.go index bdea2c525..4ebe12e85 100644 --- a/pkg/apis/platform/v1beta1/storage_spec_backend.go +++ b/pkg/apis/platform/v1beta1/storage_spec_backend.go @@ -22,6 +22,7 @@ package v1beta1 import ( shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/errors" ) @@ -31,6 +32,9 @@ type ArangoPlatformStorageSpecBackend struct { // GCS backend implements storage as a proxy to the provided GCS API endpoint GCS *ArangoPlatformStorageSpecBackendGCS `json:"gcs,omitempty"` + + // AzureBlobStorage backend implements storage as a proxy to the provided AzureBlobStorage + AzureBlobStorage *ArangoPlatformStorageSpecBackendAzureBlobStorage `json:"azureBlobStorage,omitempty"` } func (s *ArangoPlatformStorageSpecBackend) GetS3() *ArangoPlatformStorageSpecBackendS3 { @@ -40,6 +44,13 @@ func (s *ArangoPlatformStorageSpecBackend) GetS3() *ArangoPlatformStorageSpecBac return s.S3 } +func (s *ArangoPlatformStorageSpecBackend) GetAzureBlobStorage() *ArangoPlatformStorageSpecBackendAzureBlobStorage { + if s == nil || s.AzureBlobStorage == nil { + return nil + } + return s.AzureBlobStorage +} + func (s *ArangoPlatformStorageSpecBackend) GetGCS() *ArangoPlatformStorageSpecBackendGCS { if s == nil || s.GCS == nil { return nil @@ -52,11 +63,12 @@ func (s *ArangoPlatformStorageSpecBackend) Validate() error { return errors.Errorf("Backend is not specified") } - if s.S3 == nil && s.GCS == nil { + switch util.Count(true, s.S3 != nil, s.GCS != nil, s.AzureBlobStorage != nil) { + case 0: return errors.Errorf("At least one backend needs to be defined") - } - - if s.S3 != nil && s.GCS != nil { + case 1: + break + default: return errors.Errorf("Only one backend can be defined") } @@ -68,5 +80,9 @@ func (s *ArangoPlatformStorageSpecBackend) Validate() error { return shared.WithErrors(shared.PrefixResourceError("gcs", s.GCS.Validate())) } + if s.AzureBlobStorage != nil { + return shared.WithErrors(shared.PrefixResourceError("azureBlobStorage", s.AzureBlobStorage.Validate())) + } + return nil } diff --git a/pkg/apis/platform/v1beta1/storage_spec_backend_abs.go b/pkg/apis/platform/v1beta1/storage_spec_backend_abs.go new file mode 100644 index 000000000..e13de8705 --- /dev/null +++ b/pkg/apis/platform/v1beta1/storage_spec_backend_abs.go @@ -0,0 +1,128 @@ +// +// DISCLAIMER +// +// Copyright 2024-2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package v1beta1 + +import ( + "net/url" + + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + sharedApi "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +type ArangoPlatformStorageSpecBackendAzureBlobStorage struct { + // TenantID specifies the Azure TenantID + // +doc/required + TenantID *string `json:"tenantID,omitempty"` + + // AccountName specifies the Azure Storage AccountName + // used in format https://.blob.core.windows.net/ + // +doc/required + AccountName *string `json:"accountName,omitempty"` + + // Endpoint specifies the Azure Storage custom endpoint + Endpoint *string `json:"endpoint,omitempty"` + + // BucketName specifies the name of the bucket + // +doc/required + BucketName *string `json:"bucketName,omitempty"` + + // BucketPath specifies the Prefix within the bucket + // +doc/default: + BucketPrefix *string `json:"bucketPath,omitempty"` + + // CredentialsSecret specifies the Kubernetes Secret containing ClientID and ClientSecret for Azure API authorization + // +doc/required + // +doc/skip: namespace + // +doc/skip: uid + // +doc/skip: checksum + CredentialsSecret *sharedApi.Object `json:"credentialsSecret"` +} + +func (s *ArangoPlatformStorageSpecBackendAzureBlobStorage) Validate() error { + if s == nil { + s = &ArangoPlatformStorageSpecBackendAzureBlobStorage{} + } + + var errs []error + + if end := s.GetEndpoint(); end != "" { + if _, err := url.Parse(s.GetEndpoint()); err != nil { + errs = append(errs, shared.PrefixResourceErrors("endpoint", errors.Errorf("invalid URL: %s", err.Error()))) + } + } + if acc := s.GetTenantID(); acc == "" { + errs = append(errs, shared.PrefixResourceErrors("tenantID", errors.Errorf("TenantID needs to be defined"))) + } + + if acc := s.GetAccountName(); acc == "" && s.GetEndpoint() == "" { + errs = append(errs, shared.PrefixResourceErrors("accountName", errors.Errorf("AccountName needs to be defined"))) + } + + errs = append(errs, + shared.PrefixResourceErrors("credentialsSecret", s.GetCredentialsSecret().Validate()), + shared.PrefixResourceError("bucketName", shared.ValidateRequired(s.BucketName, shared.ValidateResourceName)), + ) + + return shared.WithErrors(errs...) +} + +func (s *ArangoPlatformStorageSpecBackendAzureBlobStorage) GetAccountName() string { + if s == nil || s.AccountName == nil { + return "" + } + return *s.AccountName +} + +func (s *ArangoPlatformStorageSpecBackendAzureBlobStorage) GetEndpoint() string { + if s == nil || s.Endpoint == nil { + return "" + } + return *s.Endpoint +} + +func (s *ArangoPlatformStorageSpecBackendAzureBlobStorage) GetTenantID() string { + if s == nil || s.TenantID == nil { + return "" + } + return *s.TenantID +} + +func (s *ArangoPlatformStorageSpecBackendAzureBlobStorage) GetBucketName() string { + if s == nil || s.BucketName == nil { + return "" + } + return *s.BucketName +} + +func (s *ArangoPlatformStorageSpecBackendAzureBlobStorage) GetBucketPrefix() string { + if s == nil || s.BucketPrefix == nil { + return "" + } + return *s.BucketPrefix +} + +func (s *ArangoPlatformStorageSpecBackendAzureBlobStorage) GetCredentialsSecret() *sharedApi.Object { + if s == nil || s.CredentialsSecret == nil { + return &sharedApi.Object{} + } + return s.CredentialsSecret +} diff --git a/pkg/apis/platform/v1beta1/zz_generated.deepcopy.go b/pkg/apis/platform/v1beta1/zz_generated.deepcopy.go index 1ee131cb8..5edd1fb84 100644 --- a/pkg/apis/platform/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/platform/v1beta1/zz_generated.deepcopy.go @@ -422,6 +422,11 @@ func (in *ArangoPlatformStorageSpecBackend) DeepCopyInto(out *ArangoPlatformStor *out = new(ArangoPlatformStorageSpecBackendGCS) (*in).DeepCopyInto(*out) } + if in.AzureBlobStorage != nil { + in, out := &in.AzureBlobStorage, &out.AzureBlobStorage + *out = new(ArangoPlatformStorageSpecBackendAzureBlobStorage) + (*in).DeepCopyInto(*out) + } return } @@ -435,6 +440,52 @@ func (in *ArangoPlatformStorageSpecBackend) DeepCopy() *ArangoPlatformStorageSpe return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArangoPlatformStorageSpecBackendAzureBlobStorage) DeepCopyInto(out *ArangoPlatformStorageSpecBackendAzureBlobStorage) { + *out = *in + if in.TenantID != nil { + in, out := &in.TenantID, &out.TenantID + *out = new(string) + **out = **in + } + if in.AccountName != nil { + in, out := &in.AccountName, &out.AccountName + *out = new(string) + **out = **in + } + if in.Endpoint != nil { + in, out := &in.Endpoint, &out.Endpoint + *out = new(string) + **out = **in + } + if in.BucketName != nil { + in, out := &in.BucketName, &out.BucketName + *out = new(string) + **out = **in + } + if in.BucketPrefix != nil { + in, out := &in.BucketPrefix, &out.BucketPrefix + *out = new(string) + **out = **in + } + if in.CredentialsSecret != nil { + in, out := &in.CredentialsSecret, &out.CredentialsSecret + *out = new(v1.Object) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArangoPlatformStorageSpecBackendAzureBlobStorage. +func (in *ArangoPlatformStorageSpecBackendAzureBlobStorage) DeepCopy() *ArangoPlatformStorageSpecBackendAzureBlobStorage { + if in == nil { + return nil + } + out := new(ArangoPlatformStorageSpecBackendAzureBlobStorage) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ArangoPlatformStorageSpecBackendGCS) DeepCopyInto(out *ArangoPlatformStorageSpecBackendGCS) { *out = *in diff --git a/pkg/crd/crds/platform-storage.schema.generated.yaml b/pkg/crd/crds/platform-storage.schema.generated.yaml index 23a1f2669..de69bd7ac 100644 --- a/pkg/crd/crds/platform-storage.schema.generated.yaml +++ b/pkg/crd/crds/platform-storage.schema.generated.yaml @@ -92,6 +92,41 @@ v1beta1: backend: description: Backend defines how storage is implemented properties: + azureBlobStorage: + description: AzureBlobStorage backend implements storage as a proxy to the provided AzureBlobStorage + properties: + accountName: + description: |- + AccountName specifies the Azure Storage AccountName + used in format https://.blob.core.windows.net/ + type: string + bucketName: + description: BucketName specifies the name of the bucket + type: string + bucketPath: + description: BucketPath specifies the Prefix within the bucket + type: string + credentialsSecret: + description: CredentialsSecret specifies the Kubernetes Secret containing ClientID and ClientSecret for Azure API authorization + properties: + name: + description: Name of the object + type: string + required: + - name + type: object + endpoint: + description: Endpoint specifies the Azure Storage custom endpoint + type: string + tenantID: + description: TenantID specifies the Azure TenantID + type: string + required: + - accountName + - bucketName + - credentialsSecret + - tenantID + type: object gcs: description: GCS backend implements storage as a proxy to the provided GCS API endpoint properties: diff --git a/pkg/integrations/sidecar/integration.storage.v2.go b/pkg/integrations/sidecar/integration.storage.v2.go index 371b6a40a..6e9a82fe4 100644 --- a/pkg/integrations/sidecar/integration.storage.v2.go +++ b/pkg/integrations/sidecar/integration.storage.v2.go @@ -155,6 +155,41 @@ func (i IntegrationStorageV2) Envs() ([]core.EnvVar, error) { Value: filepath.Join(mountPathStorageCredentials, utilConstants.SecretCredentialsServiceAccount), }, ) + } else if azureBlobStorage := i.Storage.Spec.GetBackend().GetAzureBlobStorage(); azureBlobStorage != nil { + envs = append(envs, + core.EnvVar{ + Name: "INTEGRATION_STORAGE_V2_TYPE", + Value: string(pbImplStorageV2.ConfigurationTypeAzure), + }, + core.EnvVar{ + Name: "INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_CLIENT_SECRET_CLIENT_ID_FILE", + Value: filepath.Join(mountPathStorageCredentials, utilConstants.SecretCredentialsAzureBlobStorageClientID), + }, + core.EnvVar{ + Name: "INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_CLIENT_SECRET_CLIENT_SECRET_FILE", + Value: filepath.Join(mountPathStorageCredentials, utilConstants.SecretCredentialsAzureBlobStorageClientSecret), + }, + core.EnvVar{ + Name: "INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_CLIENT_TENANT_ID", + Value: azureBlobStorage.GetTenantID(), + }, + core.EnvVar{ + Name: "INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_ACCOUNT_NAME", + Value: azureBlobStorage.GetAccountName(), + }, + core.EnvVar{ + Name: "INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_ENDPOINT", + Value: azureBlobStorage.GetEndpoint(), + }, + core.EnvVar{ + Name: "INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_BUCKET_NAME", + Value: azureBlobStorage.GetBucketName(), + }, + core.EnvVar{ + Name: "INTEGRATION_STORAGE_V2_AZURE_BLOB_STORAGE_BUCKET_PREFIX", + Value: azureBlobStorage.GetBucketPrefix(), + }, + ) } return i.Core.Envs(i, envs...), nil @@ -199,6 +234,16 @@ func (i IntegrationStorageV2) Volumes() ([]core.Volume, []core.VolumeMount, erro Name: mountNameStorageCredentials, MountPath: mountPathStorageCredentials, }) + } else if azureBlobStorage := i.Storage.Spec.GetBackend().GetAzureBlobStorage(); azureBlobStorage != nil { + secretObj := azureBlobStorage.GetCredentialsSecret() + if secretObj.GetNamespace(i.Storage) != i.Storage.GetNamespace() { + return nil, nil, errors.New("secrets from different namespace are not supported yet") + } + volumes = append(volumes, k8sutil.CreateVolumeWithSecret(mountNameStorageCredentials, secretObj.GetName())) + volumeMounts = append(volumeMounts, core.VolumeMount{ + Name: mountNameStorageCredentials, + MountPath: mountPathStorageCredentials, + }) } return volumes, volumeMounts, nil diff --git a/pkg/integrations/storage_v2.go b/pkg/integrations/storage_v2.go index ae5c1131f..c3b4bf05e 100644 --- a/pkg/integrations/storage_v2.go +++ b/pkg/integrations/storage_v2.go @@ -28,6 +28,7 @@ import ( pbImplStorageV2 "github.com/arangodb/kube-arangodb/integrations/storage/v2" pbStorageV2 "github.com/arangodb/kube-arangodb/integrations/storage/v2/definition" awsHelper "github.com/arangodb/kube-arangodb/pkg/util/aws" + "github.com/arangodb/kube-arangodb/pkg/util/azure" "github.com/arangodb/kube-arangodb/pkg/util/errors" "github.com/arangodb/kube-arangodb/pkg/util/gcs" "github.com/arangodb/kube-arangodb/pkg/util/svc" @@ -72,6 +73,17 @@ func (b *storageV2) Register(cmd *cobra.Command, fs FlagEnvHandler) error { fs.StringVar((*string)(&b.Configuration.GCS.Client.Provider.Type), "gcs.provider.type", string(gcs.ProviderTypeServiceAccount), "Type of the provided credentials"), fs.StringVar(&b.Configuration.GCS.Client.Provider.ServiceAccount.File, "gcs.provider.sa.file", "", "Path to the file with ServiceAccount JSON"), fs.StringVar(&b.Configuration.GCS.Client.Provider.ServiceAccount.JSON, "gcs.provider.sa.json", "", "ServiceAccount JSON"), + + fs.StringVar(&b.Configuration.AzureBlobStorage.Client.Provider.TenantID, "azure-blob-storage.client.tenant-id", "", "Azure Client Tenant ID"), + fs.StringVar(&b.Configuration.AzureBlobStorage.Client.AccountName, "azure-blob-storage.account-name", "", "AzureBlobStorage Account ID"), + fs.StringVar(&b.Configuration.AzureBlobStorage.Client.Endpoint, "azure-blob-storage.endpoint", "", "AzureBlobStorage Endpoint"), + fs.StringVar(&b.Configuration.AzureBlobStorage.BucketName, "azure-blob-storage.bucket.name", "", "Bucket name"), + fs.StringVar(&b.Configuration.AzureBlobStorage.BucketPrefix, "azure-blob-storage.bucket.prefix", "", "Bucket Prefix"), + fs.StringVar((*string)(&b.Configuration.AzureBlobStorage.Client.Provider.Type), "azure-blob-storage.client.type", string(azure.ProviderTypeSecret), "Azure Client Provider"), + fs.StringVar(&b.Configuration.AzureBlobStorage.Client.Provider.Secret.ClientID, "azure-blob-storage.client.secret.client-id", "", "Azure ClientID"), + fs.StringVar(&b.Configuration.AzureBlobStorage.Client.Provider.Secret.ClientIDFile, "azure-blob-storage.client.secret.client-id-file", "", "Azure ClientID File"), + fs.StringVar(&b.Configuration.AzureBlobStorage.Client.Provider.Secret.ClientSecret, "azure-blob-storage.client.secret.client-secret", "", "Azure ClientSecret"), + fs.StringVar(&b.Configuration.AzureBlobStorage.Client.Provider.Secret.ClientSecretFile, "azure-blob-storage.client.secret.client-secret-file", "", "Azure ClientSecret File"), ) } diff --git a/pkg/util/azure/config.go b/pkg/util/azure/config.go new file mode 100644 index 000000000..15a84577c --- /dev/null +++ b/pkg/util/azure/config.go @@ -0,0 +1,53 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package azure + +import ( + "fmt" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +type Config struct { + AccountName string + + Endpoint string + + Provider Provider +} + +func (c Config) GetCredentials() (azcore.TokenCredential, error) { + return c.Provider.GetCredentials() +} + +func (c Config) GetEndpoint() (string, error) { + if f := c.Endpoint; f != "" { + return f, nil + } + + if f := c.AccountName; f != "" { + return fmt.Sprintf("https://%s.blob.core.windows.net/", f), nil + } + + return "", errors.Errorf("account name or url not provided") +} diff --git a/pkg/util/azure/provider.go b/pkg/util/azure/provider.go new file mode 100644 index 000000000..7f9e1ece7 --- /dev/null +++ b/pkg/util/azure/provider.go @@ -0,0 +1,109 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package azure + +import ( + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +type ProviderType string + +const ( + ProviderTypeSecret ProviderType = "secret" +) + +type Provider struct { + Type ProviderType + + TenantID string + + Secret ProviderSecret +} + +func (c Provider) GetCredentials() (azcore.TokenCredential, error) { + switch c.Type { + case ProviderTypeSecret: + return c.Secret.getCredentials(c.TenantID) + } + + return nil, errors.Errorf("unable to get credentials for type '%s'", c.Type) +} + +type ProviderSecret struct { + ClientID string + ClientIDFile string + + ClientSecret string + ClientSecretFile string +} + +func (p ProviderSecret) getCredentials(tenantID string) (*azidentity.ClientSecretCredential, error) { + id, err := p.GetClientID() + if err != nil { + return nil, err + } + + secret, err := p.GetClientSecret() + if err != nil { + return nil, err + } + + return azidentity.NewClientSecretCredential(tenantID, id, secret, nil) +} + +func (p ProviderSecret) GetClientID() (string, error) { + if f := p.ClientIDFile; f != "" { + data, err := os.ReadFile(f) + if err != nil { + return "", err + } + + return string(data), nil + } + + if f := p.ClientID; f != "" { + return f, nil + } + + return "", errors.New("no client id found") +} + +func (p ProviderSecret) GetClientSecret() (string, error) { + if f := p.ClientSecretFile; f != "" { + data, err := os.ReadFile(f) + if err != nil { + return "", err + } + + return string(data), nil + } + + if f := p.ClientSecret; f != "" { + return f, nil + } + + return "", errors.New("no client secret found") +} diff --git a/pkg/util/constants/constants.go b/pkg/util/constants/constants.go index 3dc9f4870..ea7aaed8a 100644 --- a/pkg/util/constants/constants.go +++ b/pkg/util/constants/constants.go @@ -65,6 +65,9 @@ const ( SecretCredentialsServiceAccount = "serviceAccount" // Key in Secret used to store an GCS ServiceAccount File + SecretCredentialsAzureBlobStorageClientID = "clientId" // Key in Secret used to store an AzureBlobStorage ClientID + SecretCredentialsAzureBlobStorageClientSecret = "clientSecret" // Key in Secret used to store an AzureBlobStorage ClientSecret + SecretAccessPackageYaml = "accessPackage.yaml" // Key in Secret.data used to store a YAML encoded access package FinalizerDeplRemoveChildFinalizers = "database.arangodb.com/remove-child-finalizers" // Finalizer added to ArangoDeployment, indicating the need to remove finalizers from all children diff --git a/pkg/util/env_t.go b/pkg/util/env_t.go new file mode 100644 index 000000000..b7b0f9207 --- /dev/null +++ b/pkg/util/env_t.go @@ -0,0 +1,35 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +//go:build testing + +package util + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func (e EnvironmentVariable) Require(t *testing.T) string { + v, ok := e.Lookup() + require.True(t, ok) + return v +} diff --git a/pkg/util/refs.go b/pkg/util/refs.go index 6647ab74a..5e83cc7a7 100644 --- a/pkg/util/refs.go +++ b/pkg/util/refs.go @@ -238,3 +238,15 @@ func InitOptional[T any](in *T, ok bool) *T { var z T return &z } + +func Count[T comparable](value T, values ...T) int { + r := 0 + + for _, v := range values { + if value == v { + r++ + } + } + + return r +} diff --git a/pkg/util/tests/azure.go b/pkg/util/tests/azure.go new file mode 100644 index 000000000..3f3115bca --- /dev/null +++ b/pkg/util/tests/azure.go @@ -0,0 +1,76 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package tests + +import ( + "testing" + + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/azure" +) + +const ( + TestAzureClientTenant util.EnvironmentVariable = "TEST_AZURE_CLIENT_TENANT" + TestAzureClientID util.EnvironmentVariable = "TEST_AZURE_CLIENT_ID" + TestAzureClientSecret util.EnvironmentVariable = "TEST_AZURE_CLIENT_SECRET" + TestAzureAccountName util.EnvironmentVariable = "TEST_AZURE_ACCOUNT_NAME" + TestAzureEndpoint util.EnvironmentVariable = "TEST_AZURE_ENDPOINT" + TestAzureContainer util.EnvironmentVariable = "TEST_AZURE_CONTAINER" +) + +func GetAzureBlobStorageContainer(t *testing.T) string { + b, ok := TestAzureContainer.Lookup() + if !ok { + t.Skipf("Bucket does not exist") + } + + return b +} + +func GetAzureConfig(t *testing.T) azure.Config { + p := GetAzureProvider(t) + + var z azure.Config + + z.AccountName = TestAzureAccountName.GetOrDefault("") + z.Endpoint = TestAzureEndpoint.GetOrDefault("") + z.Provider = p + + return z +} + +func GetAzureProvider(t *testing.T) azure.Provider { + v, ok := TestAzureClientTenant.Lookup() + if !ok { + t.Skipf("Tenant Not Provided") + } + + var p azure.Provider + + p.TenantID = v + + p.Type = azure.ProviderTypeSecret + + p.Secret.ClientID = TestAzureClientID.Require(t) + p.Secret.ClientSecret = TestAzureClientSecret.Require(t) + + return p +}