diff --git a/azure/daprbindingobjectstore.bicep b/azure/daprbindingobjectstore.bicep new file mode 100644 index 0000000..cd98ec5 --- /dev/null +++ b/azure/daprbindingobjectstore.bicep @@ -0,0 +1,114 @@ +extension kubernetes with { + kubeConfig: '' + namespace: context.runtime.kubernetes.namespace +} as k8s +extension radius + +@description('Information about what resource is calling this Recipe. Generated by Radius. For more information visit https://docs.radapp.dev/operations/custom-recipes/') +param context object + +@description('Name of the storage account to use') +param accountName string + +@description('Optional storage account key as a secret reference to a Dapr secret store. If this is not provided, either the worload identity will be used or the account key will be fetched from the storage account reference.') +param accountKeyRef SecretStoreReference = { + secretStoreName: '' + secretKeyRef: { + name: 'accountKey' + key: 'accountKey' + } +} + +@description('Wether to use workload identity to access the storage account. Default is false') +param useWorkloadIdentity bool = false + +@description('Name of the bucket to create in the S3-compatible object store. Default is mybucket') +param bucket string = 'mybucket' + +@description('Encode binary files content in base64 before sending it to the application. Default is true') +param decodeBase64 bool = true + + + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { + name: accountName +} + +var accountKeyMetadata = useWorkloadIdentity + // If workload identity is used, the account key is not needed. + ? {} + // If workload identity is not used, the account key can be provided as a secret or a value. + // As a secret, the secret store name must be provided. + : !empty(accountKeyRef.secretStoreName) + ? { + name: 'accountKey' + secretKeyRef: accountKeyRef.secretKeyRef + } + // As a value, the account key can be fetched from the storage account reference. + : { + name: 'accountKey' + value: storageAccount.listKeys().keys[0].value + } + +// The secret store name is only needed if the account key is provided as a secret. +var daprAuthProperty = !empty(accountKeyRef.secretKeyRef) + ? { secretStore: accountKeyRef.secretStoreName } + : { secretStore: '' } + + +var daprType = 'bindings.azure.blobstorage' +var daprVersion = 'v1' +resource daprComponent 'dapr.io/Component@v1alpha1' = { + auth: daprAuthProperty + metadata: { + name: context.resource.name + } + spec: { + type: daprType + version: daprVersion + metadata: concat( + [ + { + name: 'accountName' + value: storageAccount.name + } + { + name: 'containerName' + value: bucket + } + { + name: 'decodeBase64' + value: decodeBase64 + } + ], + [accountKeyMetadata] + ) + } +} + +@description('Reference to a secret in a Dapr secret store') +type SecretStoreReference = { + @description('Name of the secret store') + secretStoreName: string + @description('Reference to a key in the secret store') + secretKeyRef: { + @description('Name of the secret') + name: string + @description('Key of the secret if it is a dictionary') + key: string + } +} + +output result object = { + // This workaround is needed because the deployment engine omits Kubernetes resources from its output. + // This allows Kubernetes resources to be cleaned up when the resource is deleted. + // Once this gap is addressed, users won't need to do this. + resources: [ + '/planes/kubernetes/local/namespaces/${daprComponent.metadata.namespace}/providers/dapr.io/Component/${daprComponent.metadata.name}' + ] + values: { + type: daprType + version: daprVersion + metadata: daprComponent.spec.metadata + } +} diff --git a/local-dev/daprbindingobjectstore.bicep b/local-dev/daprbindingobjectstore.bicep new file mode 100644 index 0000000..4eb9eee --- /dev/null +++ b/local-dev/daprbindingobjectstore.bicep @@ -0,0 +1,245 @@ +/* +Copyright 2023 The Radius Authors. + +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. +*/ +@description('Information about what resource is calling this Recipe. Generated by Radius. For more information visit https://docs.radapp.dev/operations/custom-recipes/') +param context object + +@description('Name of the bucket to create in the S3-compatible object store') +param bucket string = 'mybucket' + +@description('Tag to pull for the minio container image.') +param tag string = 'latest' + +@description('Default admin username for the MinIO object store') +param minioUser string = 'minioadmin' + +@secure() +@description('Default admin password for the MinIO object store') +#disable-next-line secure-parameter-default +param minioPassword string = 'minioadmin' + +@description('Port for the MinIO object store S3-like API') +param minioApiPort int = 9000 + +@description('Port for the MinIO object store Web UI') +param minioWebPort int = 9001 + +@description('Binary files must be converted base64 before being sent to the object store') +param encodeBase64 bool = true + +@description('Encode binary files content in base64 before sending it to the application') +param decodeBase64 bool = true + + +extension kubernetes with { + kubeConfig: '' + namespace: context.runtime.kubernetes.namespace +} as kubernetes + +var uniqueName = 'daprbindingobj-${uniqueString(context.resource.id)}' + +resource minio 'apps/Deployment@v1' = { + metadata: { + name: uniqueName + } + spec: { + replicas: 1 + selector: { + matchLabels: { + app: 'minio-app' + resource: context.resource.name + } + } + template: { + metadata: { + labels: { + app: 'minio-app' + resource: context.resource.name + 'radapp.io/application': context.application == null ? '' : context.application.name + } + } + spec: { + // This initContainer creates the bucket in the MinIO object store + // This works because the MinIO container is configured to use an emptyDir volume + // So in this case a bucket is a directory + initContainers: [ + { + name: 'bucket-creation' + image: 'busybox:1.28' + command: [ + '/bin/sh', '-c' + 'echo "Creating bucket ${bucket}" && mkdir -p /data/${bucket}' + ] + volumeMounts: [ + { + name: 'data' + mountPath: '/data' + } + ] + } + ] + containers: [ + { + name: 'minio' + image: 'minio/minio:${tag}' + args: [ + 'server' + '--address' + ':${minioApiPort}' + // Console address refers to the Web UI, somehow + '--console-address' + ':${minioWebPort}' + '/data' + ] + ports: [ + { containerPort: minioApiPort } + { containerPort: minioWebPort } + ] + env: [ + { + name: 'MINIO_ROOT_USER' + value: minioUser + } + { + name: 'MINIO_ROOT_PASSWORD' + value: minioPassword + } + // The browser animation will break port-forwarding + // https://github.com/minio/console/issues/2539 + { + name: 'MINIO_BROWSER_LOGIN_ANIMATION' + value: 'off' + } + ] + volumeMounts: [ + { + name: 'data' + mountPath: '/data' + } + ] + } + ] + volumes: [ + { + name: 'data' + emptyDir: {} // In-memory storage, not persistent (for dev/test) + } + ] + } + } + } +} + +resource svc 'core/Service@v1' = { + metadata: { + name: uniqueName + } + spec: { + type: 'ClusterIP' + selector: { + app: 'minio-app' + resource: context.resource.name + } + ports: [ + { + name: 'api' + port: minioApiPort + targetPort: minioApiPort + } + { + name: 'ui' + port: minioWebPort + targetPort: minioWebPort + } + ] + } +} + + + +var daprType = 'bindings.aws.s3' +var daprVersion = 'v1' +resource daprComponent 'dapr.io/Component@v1alpha1' = { + metadata: { + name: context.resource.name + } + spec: { + type: daprType + version: daprVersion + metadata: [ + { + name: 'endpoint' + value: '${svc.metadata.name}.${svc.metadata.namespace}.svc.cluster.local:${minioApiPort}' + } + { + name: 'bucket' + value: bucket + } + // The admin credentials are used by the Dapr sidecar to access the object store + // In a production scenario, another user with fewer permissions should be used + { + name: 'accessKey' + value: minioUser + } + { + name: 'secretKey' + value: minioPassword + } + // Binary files must be converted to base64 to be handled by Dapr + // This adds some overhead, so it can be disabled if not needed + { + name: 'encodeBase64' + value: encodeBase64 ? 'true' : 'false' + } + { + name: 'decodeBase64' + value: decodeBase64 ? 'true' : 'false' + } + // The next three values are recommended for MinIO by the documentation + // https://docs.dapr.io/reference/components-reference/supported-bindings/s3/#s3-bucket-creation + { + name: 'region' + value: 'us-east-1' // Some old Dapr versions require this to be set + } + + { + name: 'forcePathStyle' + value: true + } + + { + name: 'disableSSL' + value: true + } + ] + } +} + +output result object = { + // This workaround is needed because the deployment engine omits Kubernetes resources from its output. + // This allows Kubernetes resources to be cleaned up when the resource is deleted. + // Once this gap is addressed, users won't need to do this. + resources: [ + '/planes/kubernetes/local/namespaces/${svc.metadata.namespace}/providers/core/Service/${svc.metadata.name}' + '/planes/kubernetes/local/namespaces/${minio.metadata.namespace}/providers/apps/Deployment/${minio.metadata.name}' + '/planes/kubernetes/local/namespaces/${daprComponent.metadata.namespace}/providers/dapr.io/Component/${daprComponent.metadata.name}' + ] + values: { + type: daprType + version: daprVersion + metadata: daprComponent.spec.metadata + } +} +