Merge branch 'master' into BUDI-7580/account_portal_submodule
This commit is contained in:
commit
1e267fca5b
4
LICENSE
4
LICENSE
|
@ -1,7 +1,9 @@
|
|||
Copyright 2019-2021, Budibase Ltd.
|
||||
Copyright 2019-2023, Budibase Ltd.
|
||||
|
||||
Each Budibase package has its own license, please check the license file in each package.
|
||||
|
||||
You can consider Budibase to be GPLv3 licensed overall.
|
||||
|
||||
The apps that you build with Budibase do not package any GPLv3 licensed code, thus do not fall under those restrictions.
|
||||
|
||||
Budibase ships with Structured Query Server, by The Neighbourhoodie Software GmbH. This license for this can be found at ./SQS_LICENSE
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
FORM OF CUSTOMER LICENCE
|
||||
|
||||
Budibase hereby grants the Customer a worldwide, royalty free, non-exclusive,
|
||||
perpetual (for the lifetime of the intellectual property rights contained in the Product)
|
||||
right and title to utilise the binary code of the The Neighbourhoodie Software GmbH
|
||||
Structured Query Server software product (Product) for its own internal business
|
||||
purposes (the Purpose) only (the Licence). The Product has the function of bringing a
|
||||
CouchDB database (NoSQL database) into an SQL database form (SQLite) and thereby
|
||||
making it usable for complex queries - which originally could only be displayed in an
|
||||
SQL database. By indexing in SQLite and a server that is tailored to it, the Product
|
||||
enables the use of CouchDB with SQL queries.
|
||||
The Licence shall not permit sub-licensing, resale or transfer of the Product to third
|
||||
parties, other than sub-licensing to the Customer’s direct contractors for the purposes
|
||||
of utilizing the Product as contemplated above.
|
||||
The Licence shall not permit the adaptation, modification, decompilation, reverse
|
||||
engineering or similar activities with respect to the Product.
|
||||
This licence is granted to the Customer only, although Customer and its Affiliates’
|
||||
employees, servants and agents shall be entitled to utilize the Product within the scope
|
||||
of the Licence for the Customer’s Purpose only.
|
||||
Reproduction is not permitted to users, except for reproductions that are necessary for
|
||||
the use of the product under the licence described above. These conditions apply to the
|
||||
product regardless of the form in which we make the product available and on which
|
||||
devices it is installed and/or with which devices it is ultimately used. Depending on the
|
||||
product variant or intended use, certain technical requirements in the IT infrastructure
|
||||
must be satisfied as a prerequisite for use.
|
||||
The law of the Northern Ireland applies exclusively to this licence, and the courts of
|
||||
Northern Ireland shall have exclusive jurisdiction, save that we reserve a right to sue
|
||||
you in the jurisdiction in which you are based. The application of the UN Sales
|
||||
Convention (CISG) is excluded.
|
||||
The invalidity of any part of this licence does not affect the validity of the remaining
|
||||
regulations.
|
|
@ -1,9 +1,6 @@
|
|||
dependencies:
|
||||
- name: couchdb
|
||||
repository: https://apache.github.io/couchdb-helm
|
||||
version: 3.3.4
|
||||
- name: ingress-nginx
|
||||
repository: https://kubernetes.github.io/ingress-nginx
|
||||
version: 4.0.13
|
||||
digest: sha256:20892705c2d8e64c98257d181063a514ac55013e2b43399a6e54868a97f97845
|
||||
generated: "2021-12-30T18:55:30.878411Z"
|
||||
version: 4.3.0
|
||||
digest: sha256:94449a7f195b186f5af33ec5aa66d58b36bede240fae710f021ca87837b30606
|
||||
generated: "2023-11-20T17:43:02.777596Z"
|
||||
|
|
|
@ -17,10 +17,6 @@ version: 0.0.0
|
|||
appVersion: 0.0.0
|
||||
dependencies:
|
||||
- name: couchdb
|
||||
version: 3.3.4
|
||||
version: 4.3.0
|
||||
repository: https://apache.github.io/couchdb-helm
|
||||
condition: services.couchdb.enabled
|
||||
- name: ingress-nginx
|
||||
version: 4.0.13
|
||||
repository: https://kubernetes.github.io/ingress-nginx
|
||||
condition: ingress.nginx
|
||||
|
|
|
@ -1,39 +1,217 @@
|
|||
# Budibase
|
||||
# budibase
|
||||
|
||||
[Budibase](https://budibase.com/) Budibase is an open source low-code platform, helping thousands of teams build apps for their workplace in minutes.
|
||||
|
||||
## TL;DR;
|
||||
```console
|
||||
$ cd chart
|
||||
$ helm install budibase .
|
||||
```
|
||||
|
||||
## Introduction
|
||||
|
||||
This chart bootstraps a [Budibase](https://budibase.com/) deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
|
||||
Budibase is an open source low-code platform, helping thousands of teams build apps for their workplace in minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- helm v3 or above
|
||||
- `helm` v3 or above
|
||||
- Kubernetes 1.4+
|
||||
- PV provisioner support in the underlying infrastructure (with persistence storage enabled)
|
||||
- A storage controller (if you want to use persistent storage)
|
||||
- An ingress controller (if you want to define an `Ingress` resource)
|
||||
- `metrics-server` (if you want to make use of horizontal pod autoscaling)
|
||||
|
||||
## Installing the Chart
|
||||
## Chart dependencies
|
||||
|
||||
To install the chart with the release name `budi-release`:
|
||||
This chart depends on the official Apache CouchDB chart. You can see its
|
||||
documentation here:
|
||||
<https://github.com/apache/couchdb-helm/tree/couchdb-4.3.0/couchdb>.
|
||||
|
||||
## Upgrading
|
||||
|
||||
### `2.x` to `3.0.0`
|
||||
|
||||
We made a number of breaking changes in this release to make the chart more
|
||||
idiomatic and easier to use.
|
||||
|
||||
1. We no longer bundle `ingress-nginx`. If you were relying on this to supply
|
||||
an ingress controller to your cluster, you will now need to deploy that
|
||||
separately. You'll find guidance for that here:
|
||||
<https://kubernetes.github.io/ingress-nginx/>.
|
||||
2. We've upgraded the version of the [CouchDB chart](https://github.com/apache/couchdb-helm)
|
||||
we use from `3.3.4` to `4.3.0`. The primary motivation for this was to align
|
||||
the CouchDB chart used with the CouchDB version used, which has also updated
|
||||
from 3.1.1 to 3.2.1. Additionally, we're moving away from the official CouchDB
|
||||
to one we're building ourselves.
|
||||
3. We've separated out the supplied AWS ALB ingress resource for those deploying
|
||||
into EKS. Where previously you enabled this by setting `ingress.enabled: false`
|
||||
and `ingress.aws: true`, you now set `awsAlbIngress.enabled: true` and all
|
||||
configuration for it is under `awsAlbIngress`.
|
||||
4. The `HorizontalPodAutoscaler` that was configured at `hpa.enabled: true` has
|
||||
been split into 3 separate HPAs, one for each of `apps`, `worker`, and `proxy`.
|
||||
They are configured at `services.{apps,worker,proxy}.autoscaling`.
|
||||
|
||||
## Installing
|
||||
|
||||
To install the chart from our repository:
|
||||
|
||||
```console
|
||||
$ helm install budi-release .
|
||||
$ helm repo add budibase https://budibase.github.io/budibase/
|
||||
$ helm repo update
|
||||
$ helm install --create-namespace --namespace budibase budibase budibase/budibase
|
||||
```
|
||||
|
||||
The command deploys Budibase on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation.
|
||||
|
||||
> **Tip**: List all releases using `helm list`
|
||||
|
||||
## Uninstalling the Chart
|
||||
|
||||
To uninstall/delete the `my-release` deployment:
|
||||
To install the chart from this repo:
|
||||
|
||||
```console
|
||||
$ helm delete my-release
|
||||
$ git clone git@github.com:budibase/budibase.git
|
||||
$ cd budibase/charts/budibase
|
||||
$ helm install --create-namespace --namespace budibase budibase .
|
||||
```
|
||||
|
||||
## Example minimal configuration
|
||||
|
||||
Here's an example `values.yaml` that would get a Budibase instance running in a home
|
||||
cluster using an nginx ingress controller and NFS as cluster storage (basically one of our
|
||||
staff's homelabs).
|
||||
|
||||
<details>
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
hosts:
|
||||
- host: budibase.local # set this to whatever DNS name you'd use
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: proxy-service
|
||||
port:
|
||||
number: 10000
|
||||
path: /
|
||||
pathType: Prefix
|
||||
|
||||
couchdb:
|
||||
persistentVolume:
|
||||
enabled: true
|
||||
storageClass: "nfs-client"
|
||||
adminPassword: admin
|
||||
|
||||
services:
|
||||
objectStore:
|
||||
storageClass: "nfs-client"
|
||||
redis:
|
||||
storageClass: "nfs-client"
|
||||
```
|
||||
|
||||
If you wanted to use this when bringing up Budibase in your own cluster, you could save it
|
||||
to your hard disk and run the following:
|
||||
|
||||
```console
|
||||
$ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Configuring
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| affinity | object | `{}` | Sets the affinity for all pods created by this chart. Should not ordinarily need to be changed. See <https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/> for more information on affinity. |
|
||||
| awsAlbIngress.certificateArn | string | `""` | If you're wanting to use HTTPS, you'll need to create an ACM certificate and specify the ARN here. |
|
||||
| awsAlbIngress.enabled | bool | `false` | Whether to create an ALB Ingress resource pointing to the Budibase proxy. Requires the AWS ALB Ingress Controller. |
|
||||
| couchdb.clusterSize | int | `1` | The number of replicas to run in the CouchDB cluster. We set this to 1 by default to make things simpler, but you can set it to 3 if you need a high-availability CouchDB cluster. |
|
||||
| couchdb.couchdbConfig.couchdb.uuid | string | `"budibase-couchdb"` | Unique identifier for this CouchDB server instance. You shouldn't need to change this. |
|
||||
| couchdb.image | object | `{}` | We use a custom CouchDB image for running Budibase and we don't support using any other CouchDB image. You shouldn't change this, and if you do we can't guarantee that Budibase will work. |
|
||||
| globals.apiEncryptionKey | string | `""` | Used for encrypting API keys and environment variables when stored in the database. You don't need to set this if `createSecrets` is true. |
|
||||
| globals.appVersion | string | `""` | The version of Budibase to deploy. Defaults to what's specified by {{ .Chart.AppVersion }}. Ends up being used as the image version tag for the apps, proxy, and worker images. |
|
||||
| globals.automationMaxIterations | string | `"200"` | The maximum number of iterations allows for an automation loop step. You can read more about looping here: <https://docs.budibase.com/docs/looping>. |
|
||||
| globals.budibaseEnv | string | `"PRODUCTION"` | Sets the environment variable BUDIBASE_ENVIRONMENT for the apps and worker pods. Should not ordinarily need to be changed. |
|
||||
| globals.cookieDomain | string | `""` | Sets the domain attribute of the cookie that Budibase uses to store session information. See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_where_cookies_are_sent> for details on why you might want to set this. |
|
||||
| globals.createSecrets | bool | `true` | Create an internal API key, JWT secret, object store access key and secret, and store them in a Kubernetes `Secret`. |
|
||||
| globals.enableAnalytics | string | `"1"` | Whether to enable analytics or not. You can read more about our analytics here: <https://docs.budibase.com/docs/analytics>. |
|
||||
| globals.google | object | `{"clientId":"","secret":""}` | Google OAuth settings. These can also be set in the Budibase UI, see <https://docs.budibase.com/docs/sso-with-google> for details. |
|
||||
| globals.google.clientId | string | `""` | Client ID of your Google OAuth app. |
|
||||
| globals.google.secret | string | `""` | Client secret of your Google OAuth app. |
|
||||
| globals.httpMigrations | string | `"0"` | Whether or not to enable doing data migrations over the HTTP API. If this is set to "0", migrations are run on startup. You shouldn't ordinarily need to change this. |
|
||||
| globals.internalApiKey | string | `""` | API key used for internal Budibase API calls. You don't need to set this if `createSecrets` is true. |
|
||||
| globals.internalApiKeyFallback | string | `""` | A fallback value for `internalApiKey`. If you're rotating your encryption key, you can set this to the old value for the duration of the rotation. |
|
||||
| globals.jwtSecret | string | `""` | Secret used for signing JWTs. You don't need to set this if `createSecrets` is true. |
|
||||
| globals.jwtSecretFallback | string | `""` | A fallback value for `jwtSecret`. If you're rotating your JWT secret, you can set this to the old value for the duration of the rotation. |
|
||||
| globals.platformUrl | string | `""` | Set the `platformUrl` binding. You can also do this in Settings > Organisation if you are self-hosting. |
|
||||
| globals.smtp.enabled | bool | `false` | Whether to enable SMTP or not. |
|
||||
| globals.smtp.from | string | `""` | The email address to use in the "From:" field of emails sent by Budibase. |
|
||||
| globals.smtp.host | string | `""` | The hostname of your SMTP server. |
|
||||
| globals.smtp.password | string | `""` | The password to use when authenticating with your SMTP server. |
|
||||
| globals.smtp.port | string | `"587"` | The port of your SMTP server. |
|
||||
| globals.smtp.user | string | `""` | The username to use when authenticating with your SMTP server. |
|
||||
| globals.tenantFeatureFlags | string | `"*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"` | Sets what feature flags are enabled and for which tenants. Should not ordinarily need to be changed. |
|
||||
| imagePullSecrets | list | `[]` | Passed to all pods created by this chart. Should not ordinarily need to be changed. |
|
||||
| ingress.className | string | `""` | What ingress class to use. |
|
||||
| ingress.enabled | bool | `true` | Whether to create an Ingress resource pointing to the Budibase proxy. |
|
||||
| ingress.hosts | list | `[]` | Standard hosts block for the Ingress resource. Defaults to pointing to the Budibase proxy. |
|
||||
| nameOverride | string | `""` | Override the name of the deploymen. Defaults to {{ .Chart.Name }}. |
|
||||
| service.port | int | `10000` | Port to expose on the service. |
|
||||
| service.type | string | `"ClusterIP"` | Service type for the service that points to the main Budibase proxy pod. |
|
||||
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
|
||||
| services.apps.autoscaling.enabled | bool | `false` | Whether to enable horizontal pod autoscaling for the apps service. |
|
||||
| services.apps.autoscaling.maxReplicas | int | `10` | |
|
||||
| services.apps.autoscaling.minReplicas | int | `1` | |
|
||||
| services.apps.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the apps service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the apps pods. |
|
||||
| services.apps.httpLogging | int | `1` | Whether or not to log HTTP requests to the apps service. |
|
||||
| services.apps.livenessProbe | object | HTTP health checks. | Liveness probe configuration for apps pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||
| services.apps.logLevel | string | `"info"` | The log level for the apps service. |
|
||||
| services.apps.readinessProbe | object | HTTP health checks. | Readiness probe configuration for apps pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||
| services.apps.replicaCount | int | `1` | The number of apps replicas to run. |
|
||||
| services.apps.resources | object | `{}` | The resources to use for apps pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
||||
| services.apps.startupProbe | object | HTTP health checks. | Startup probe configuration for apps pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||
| services.couchdb.backup.enabled | bool | `false` | Whether or not to enable periodic CouchDB backups. This works by replicating to another CouchDB instance. |
|
||||
| services.couchdb.backup.interval | string | `""` | Backup interval in seconds |
|
||||
| services.couchdb.backup.resources | object | `{}` | The resources to use for CouchDB backup pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
||||
| services.couchdb.backup.target | string | `""` | Target couchDB instance to back up to, either a hostname or an IP address. |
|
||||
| services.couchdb.enabled | bool | `true` | Whether or not to spin up a CouchDB instance in your cluster. True by default, and the configuration for the CouchDB instance is under the `couchdb` key at the root of this file. You can see what options are available to you by looking at the official CouchDB Helm chart: <https://github.com/apache/couchdb-helm/tree/couchdb-4.3.0/couchdb>. |
|
||||
| services.couchdb.port | int | `5984` | |
|
||||
| services.dns | string | `"cluster.local"` | The DNS suffix to use for service discovery. You only need to change this if you've configured your cluster to use a different DNS suffix. |
|
||||
| services.objectStore.accessKey | string | `""` | AWS_ACCESS_KEY if using S3 |
|
||||
| services.objectStore.browser | bool | `true` | Whether to enable the Minio web console or not. If you're exposing Minio to the Internet (via a custom Ingress record, for example), you should set this to false. If you're only exposing Minio to your cluster, you can leave this as true. |
|
||||
| services.objectStore.cloudfront.cdn | string | `""` | Set the url of a distribution to enable cloudfront. |
|
||||
| services.objectStore.cloudfront.privateKey64 | string | `""` | Base64 encoded private key for the above public key. |
|
||||
| services.objectStore.cloudfront.publicKeyId | string | `""` | ID of public key stored in cloudfront. |
|
||||
| services.objectStore.minio | bool | `true` | Set to false if using another object store, such as S3. You will need to set `services.objectStore.url` to point to your bucket if you do this. |
|
||||
| services.objectStore.region | string | `""` | AWS_REGION if using S3 |
|
||||
| services.objectStore.resources | object | `{}` | The resources to use for Minio pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
||||
| services.objectStore.secretKey | string | `""` | AWS_SECRET_ACCESS_KEY if using S3 |
|
||||
| services.objectStore.storage | string | `"100Mi"` | How much storage to give Minio in its PersistentVolumeClaim. |
|
||||
| services.objectStore.storageClass | string | `""` | If defined, storageClassName: <storageClass> If set to "-", storageClassName: "", which disables dynamic provisioning If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. |
|
||||
| services.objectStore.url | string | `"http://minio-service:9000"` | URL to use for object storage. Only change this if you're using an external object store, such as S3. Remember to set `minio: false` if you do this. |
|
||||
| services.proxy.autoscaling.enabled | bool | `false` | Whether to enable horizontal pod autoscaling for the proxy service. |
|
||||
| services.proxy.autoscaling.maxReplicas | int | `10` | |
|
||||
| services.proxy.autoscaling.minReplicas | int | `1` | |
|
||||
| services.proxy.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the proxy service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the proxy pods. |
|
||||
| services.proxy.livenessProbe | object | HTTP health checks. | Liveness probe configuration for proxy pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||
| services.proxy.readinessProbe | object | HTTP health checks. | Readiness probe configuration for proxy pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||
| services.proxy.replicaCount | int | `1` | The number of proxy replicas to run. |
|
||||
| services.proxy.resources | object | `{}` | The resources to use for proxy pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
||||
| services.proxy.startupProbe | object | HTTP health checks. | Startup probe configuration for proxy pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||
| services.redis.enabled | bool | `true` | Whether or not to deploy a Redis pod into your cluster. |
|
||||
| services.redis.password | string | `"budibase"` | The password to use when connecting to Redis. It's recommended that you change this from the default if you're running Redis in-cluster. |
|
||||
| services.redis.port | int | `6379` | Port to expose Redis on. |
|
||||
| services.redis.resources | object | `{}` | The resources to use for Redis pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
||||
| services.redis.storage | string | `"100Mi"` | How much persistent storage to allocate to Redis. |
|
||||
| services.redis.storageClass | string | `""` | If defined, storageClassName: <storageClass> If set to "-", storageClassName: "", which disables dynamic provisioning If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. |
|
||||
| services.redis.url | string | `""` | If you choose to run Redis externally to this chart, you can specify the connection details here. |
|
||||
| services.worker.autoscaling.enabled | bool | `false` | Whether to enable horizontal pod autoscaling for the worker service. |
|
||||
| services.worker.autoscaling.maxReplicas | int | `10` | |
|
||||
| services.worker.autoscaling.minReplicas | int | `1` | |
|
||||
| services.worker.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the worker service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the worker pods. |
|
||||
| services.worker.httpLogging | int | `1` | Whether or not to log HTTP requests to the worker service. |
|
||||
| services.worker.livenessProbe | object | HTTP health checks. | Liveness probe configuration for worker pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||
| services.worker.logLevel | string | `"info"` | The log level for the worker service. |
|
||||
| services.worker.readinessProbe | object | HTTP health checks. | Readiness probe configuration for worker pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||
| services.worker.replicaCount | int | `1` | The number of worker replicas to run. |
|
||||
| services.worker.resources | object | `{}` | The resources to use for worker pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
||||
| services.worker.startupProbe | object | HTTP health checks. | Startup probe configuration for worker pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||
| tolerations | list | `[]` | Sets the tolerations for all pods created by this chart. Should not ordinarily need to be changed. See <https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/> for more information on tolerations. |
|
||||
|
||||
## Uninstalling
|
||||
|
||||
To uninstall the chart, assuming you named the release `budibase` (both commands in the installation section do so):
|
||||
|
||||
```console
|
||||
$ helm uninstall --namespace budibase budibase
|
||||
```
|
||||
|
||||
----------------------------------------------
|
||||
Autogenerated from chart metadata using [helm-docs v1.11.3](https://github.com/norwoodj/helm-docs/releases/v1.11.3)
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
{{ template "chart.header" . }}
|
||||
{{ template "chart.description" . }}
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `helm` v3 or above
|
||||
- Kubernetes 1.4+
|
||||
- A storage controller (if you want to use persistent storage)
|
||||
- An ingress controller (if you want to define an `Ingress` resource)
|
||||
- `metrics-server` (if you want to make use of horizontal pod autoscaling)
|
||||
|
||||
## Chart dependencies
|
||||
|
||||
This chart depends on the official Apache CouchDB chart. You can see its
|
||||
documentation here:
|
||||
<https://github.com/apache/couchdb-helm/tree/couchdb-4.3.0/couchdb>.
|
||||
|
||||
## Upgrading
|
||||
|
||||
### `2.x` to `3.0.0`
|
||||
|
||||
We made a number of breaking changes in this release to make the chart more
|
||||
idiomatic and easier to use.
|
||||
|
||||
1. We no longer bundle `ingress-nginx`. If you were relying on this to supply
|
||||
an ingress controller to your cluster, you will now need to deploy that
|
||||
separately. You'll find guidance for that here:
|
||||
<https://kubernetes.github.io/ingress-nginx/>.
|
||||
2. We've upgraded the version of the [CouchDB chart](https://github.com/apache/couchdb-helm)
|
||||
we use from `3.3.4` to `4.3.0`. The primary motivation for this was to align
|
||||
the CouchDB chart used with the CouchDB version used, which has also updated
|
||||
from 3.1.1 to 3.2.1. Additionally, we're moving away from the official CouchDB
|
||||
to one we're building ourselves.
|
||||
3. We've separated out the supplied AWS ALB ingress resource for those deploying
|
||||
into EKS. Where previously you enabled this by setting `ingress.enabled: false`
|
||||
and `ingress.aws: true`, you now set `awsAlbIngress.enabled: true` and all
|
||||
configuration for it is under `awsAlbIngress`.
|
||||
4. The `HorizontalPodAutoscaler` that was configured at `hpa.enabled: true` has
|
||||
been split into 3 separate HPAs, one for each of `apps`, `worker`, and `proxy`.
|
||||
They are configured at `services.{apps,worker,proxy}.autoscaling`.
|
||||
|
||||
## Installing
|
||||
|
||||
To install the chart from our repository:
|
||||
|
||||
```console
|
||||
$ helm repo add budibase https://budibase.github.io/budibase/
|
||||
$ helm repo update
|
||||
$ helm install --create-namespace --namespace budibase budibase budibase/budibase
|
||||
```
|
||||
|
||||
To install the chart from this repo:
|
||||
|
||||
```console
|
||||
$ git clone git@github.com:budibase/budibase.git
|
||||
$ cd budibase/charts/budibase
|
||||
$ helm install --create-namespace --namespace budibase budibase .
|
||||
```
|
||||
|
||||
## Example minimal configuration
|
||||
|
||||
Here's an example `values.yaml` that would get a Budibase instance running in a home
|
||||
cluster using an nginx ingress controller and NFS as cluster storage (basically one of our
|
||||
staff's homelabs).
|
||||
|
||||
<details>
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
hosts:
|
||||
- host: budibase.local # set this to whatever DNS name you'd use
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: proxy-service
|
||||
port:
|
||||
number: 10000
|
||||
path: /
|
||||
pathType: Prefix
|
||||
|
||||
couchdb:
|
||||
persistentVolume:
|
||||
enabled: true
|
||||
storageClass: "nfs-client"
|
||||
adminPassword: admin
|
||||
|
||||
services:
|
||||
objectStore:
|
||||
storageClass: "nfs-client"
|
||||
redis:
|
||||
storageClass: "nfs-client"
|
||||
```
|
||||
|
||||
If you wanted to use this when bringing up Budibase in your own cluster, you could save it
|
||||
to your hard disk and run the following:
|
||||
|
||||
```console
|
||||
$ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Configuring
|
||||
|
||||
{{ template "chart.valuesTable" . }}
|
||||
|
||||
## Uninstalling
|
||||
|
||||
To uninstall the chart, assuming you named the release `budibase` (both commands in the installation section do so):
|
||||
|
||||
```console
|
||||
$ helm uninstall --namespace budibase budibase
|
||||
```
|
||||
|
||||
{{ template "helm-docs.versionFooter" . }}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,4 +1,4 @@
|
|||
{{- if .Values.ingress.aws }}
|
||||
{{- if .Values.awsAlbIngress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
|
@ -9,22 +9,22 @@ metadata:
|
|||
alb.ingress.kubernetes.io/target-type: ip
|
||||
alb.ingress.kubernetes.io/success-codes: '200'
|
||||
alb.ingress.kubernetes.io/healthcheck-path: '/health'
|
||||
{{- if .Values.ingress.certificateArn }}
|
||||
{{- if .Values.awsAlbIngress.certificateArn }}
|
||||
alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
|
||||
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
|
||||
alb.ingress.kubernetes.io/certificate-arn: {{ .Values.ingress.certificateArn }}
|
||||
alb.ingress.kubernetes.io/certificate-arn: {{ .Values.awsAlbIngress.certificateArn }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.sslPolicy }}
|
||||
alb.ingress.kubernetes.io/actions.ssl-policy: {{ .Values.ingress.sslPolicy }}
|
||||
{{- if .Values.awsAlbIngress.sslPolicy }}
|
||||
alb.ingress.kubernetes.io/actions.ssl-policy: {{ .Values.awsAlbIngress.sslPolicy }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.securityGroups }}
|
||||
alb.ingress.kubernetes.io/security-groups: {{ .Values.ingress.securityGroups }}
|
||||
{{- if .Values.awsAlbIngress.securityGroups }}
|
||||
alb.ingress.kubernetes.io/security-groups: {{ .Values.awsAlbIngress.securityGroups }}
|
||||
{{- end }}
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
{{- if .Values.ingress.certificateArn }}
|
||||
{{- if .Values.awsAlbIngress.certificateArn }}
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
|
|
|
@ -2,12 +2,9 @@ apiVersion: apps/v1
|
|||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
{{ if .Values.services.apps.deploymentAnnotations }}
|
||||
{{- toYaml .Values.services.apps.deploymentAnnotations | indent 4 -}}
|
||||
{{ end }}
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: app-service
|
||||
{{ if .Values.services.apps.deploymentLabels }}
|
||||
|
@ -24,12 +21,9 @@ spec:
|
|||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
{{ if .Values.services.apps.templateAnnotations }}
|
||||
{{- toYaml .Values.services.apps.templateAnnotations | indent 8 -}}
|
||||
{{ end }}
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: app-service
|
||||
{{ if .Values.services.apps.templateLabels }}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
{{- if .Values.services.apps.autoscaling.enabled }}
|
||||
apiVersion: {{ ternary "autoscaling/v2" "autoscaling/v2beta2" (.Capabilities.APIVersions.Has "autoscaling/v2") }}
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "budibase.fullname" . }}-apps
|
||||
labels:
|
||||
{{- include "budibase.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: app-service
|
||||
minReplicas: {{ .Values.services.apps.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.services.apps.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.services.apps.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.services.apps.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.services.apps.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.services.apps.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -1,10 +1,6 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: app-service
|
||||
name: app-service
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app.kubernetes.io/name: couchdb-backup
|
||||
name: couchdb-backup
|
||||
|
@ -18,10 +14,6 @@ spec:
|
|||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app.kubernetes.io/name: couchdb-backup
|
||||
spec:
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "budibase.fullname" . }}
|
||||
labels:
|
||||
{{- include "budibase.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "budibase.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -2,7 +2,6 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: minio-data
|
||||
name: minio-data
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: minio-service
|
||||
name: minio-service
|
||||
|
@ -18,10 +14,6 @@ spec:
|
|||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: minio-service
|
||||
spec:
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: minio-service
|
||||
name: minio-service
|
||||
|
|
|
@ -2,12 +2,9 @@ apiVersion: apps/v1
|
|||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
{{ if .Values.services.proxy.deploymentAnnotations }}
|
||||
{{- toYaml .Values.services.proxy.deploymentAnnotations | indent 4 -}}
|
||||
{{ end }}
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app.kubernetes.io/name: budibase-proxy
|
||||
{{ if .Values.services.proxy.deploymentLabels }}
|
||||
|
@ -19,17 +16,15 @@ spec:
|
|||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: budibase-proxy
|
||||
minReadySeconds: 10
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
{{ if .Values.services.proxy.templateAnnotations }}
|
||||
{{- toYaml .Values.services.proxy.templateAnnotations | indent 8 -}}
|
||||
{{ end }}
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app.kubernetes.io/name: budibase-proxy
|
||||
{{ if .Values.services.proxy.templateLabels }}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
{{- if .Values.services.proxy.autoscaling.enabled }}
|
||||
apiVersion: {{ ternary "autoscaling/v2" "autoscaling/v2beta2" (.Capabilities.APIVersions.Has "autoscaling/v2") }}
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "budibase.fullname" . }}-proxy
|
||||
labels:
|
||||
{{- include "budibase.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: proxy-service
|
||||
minReplicas: {{ .Values.services.proxy.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.services.proxy.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.services.proxy.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.services.proxy.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.services.proxy.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.services.proxy.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -1,10 +1,6 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app.kubernetes.io/name: budibase-proxy
|
||||
name: proxy-service
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: redis-data
|
||||
name: redis-data
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: redis-service
|
||||
name: redis-service
|
||||
|
@ -18,10 +14,6 @@ spec:
|
|||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: redis-service
|
||||
spec:
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: redis-service
|
||||
name: redis-service
|
||||
|
|
|
@ -2,12 +2,9 @@ apiVersion: apps/v1
|
|||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
{{ if .Values.services.worker.deploymentAnnotations }}
|
||||
{{- toYaml .Values.services.worker.deploymentAnnotations | indent 4 -}}
|
||||
{{ end }}
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: worker-service
|
||||
{{ if .Values.services.worker.deploymentLabels }}
|
||||
|
@ -24,12 +21,9 @@ spec:
|
|||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
{{ if .Values.services.worker.templateAnnotations }}
|
||||
{{- toYaml .Values.services.worker.templateAnnotations | indent 8 -}}
|
||||
{{ end }}
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: worker-service
|
||||
{{ if .Values.services.worker.templateLabels }}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
{{- if .Values.services.worker.autoscaling.enabled }}
|
||||
apiVersion: {{ ternary "autoscaling/v2" "autoscaling/v2beta2" (.Capabilities.APIVersions.Has "autoscaling/v2") }}
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "budibase.fullname" . }}-worker
|
||||
labels:
|
||||
{{- include "budibase.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: worker-service
|
||||
minReplicas: {{ .Values.services.worker.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.services.worker.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.services.worker.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.services.worker.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.services.worker.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.services.worker.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -1,10 +1,6 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
kompose.cmd: kompose convert
|
||||
kompose.version: 1.21.0 (992df58d8)
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: worker-service
|
||||
name: worker-service
|
||||
|
|
|
@ -1,56 +1,32 @@
|
|||
# Default values for budibase.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
image:
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
# -- Passed to all pods created by this chart. Should not ordinarily need to be changed.
|
||||
imagePullSecrets: []
|
||||
# -- Override the name of the deploymen. Defaults to {{ .Chart.Name }}.
|
||||
nameOverride: ""
|
||||
# fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
# -- Specifies whether a service account should be created
|
||||
create: true
|
||||
# Annotations to add to the service account
|
||||
# -- Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# -- The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
{}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext:
|
||||
{}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
# -- Service type for the service that points to the main Budibase proxy pod.
|
||||
type: ClusterIP
|
||||
# -- Port to expose on the service.
|
||||
port: 10000
|
||||
|
||||
ingress:
|
||||
# -- Whether to create an Ingress resource pointing to the Budibase proxy.
|
||||
enabled: true
|
||||
aws: false
|
||||
nginx: true
|
||||
certificateArn: ""
|
||||
# -- What ingress class to use.
|
||||
className: ""
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
nginx.ingress.kubernetes.io/client-max-body-size: 150M
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: 50m
|
||||
# -- Standard hosts block for the Ingress resource. Defaults to pointing to the Budibase proxy.
|
||||
hosts:
|
||||
- host: # change if using custom domain
|
||||
# @ignore
|
||||
- host:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
|
@ -60,361 +36,426 @@ ingress:
|
|||
port:
|
||||
number: 10000
|
||||
|
||||
autoscaling:
|
||||
awsAlbIngress:
|
||||
# -- Whether to create an ALB Ingress resource pointing to the Budibase proxy. Requires the AWS ALB Ingress Controller.
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
# -- If you're wanting to use HTTPS, you'll need to create an ACM certificate and specify the ARN here.
|
||||
certificateArn: ""
|
||||
|
||||
# -- Sets the tolerations for all pods created by this chart. Should not ordinarily need to be changed.
|
||||
# See <https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/> for more information
|
||||
# on tolerations.
|
||||
tolerations: []
|
||||
|
||||
# -- Sets the affinity for all pods created by this chart. Should not ordinarily
|
||||
# need to be changed. See
|
||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/>
|
||||
# for more information on affinity.
|
||||
affinity: {}
|
||||
|
||||
globals:
|
||||
appVersion: "" # Use as an override to .Chart.AppVersion
|
||||
# -- The version of Budibase to deploy. Defaults to what's specified by {{ .Chart.AppVersion }}.
|
||||
# Ends up being used as the image version tag for the apps, proxy, and worker images.
|
||||
appVersion: ""
|
||||
# -- Sets the environment variable BUDIBASE_ENVIRONMENT for the apps and worker pods. Should not
|
||||
# ordinarily need to be changed.
|
||||
budibaseEnv: PRODUCTION
|
||||
# -- Sets what feature flags are enabled and for which tenants. Should not ordinarily need to be
|
||||
# changed.
|
||||
tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
|
||||
# -- Whether to enable analytics or not. You can read more about our analytics here:
|
||||
# <https://docs.budibase.com/docs/analytics>.
|
||||
enableAnalytics: "1"
|
||||
# @ignore (only used if enableAnalytics is set to 1)
|
||||
posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU"
|
||||
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
||||
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
||||
offlineMode: "0" # set to 1 to enable offline mode
|
||||
# @ignore (should not normally need to be changed, we only set this to "0"
|
||||
# when deploying to our Cloud environment)
|
||||
selfHosted: "1"
|
||||
# @ignore (doesn't work out of the box for self-hosted users, only meant for Budicloud)
|
||||
multiTenancy: "0"
|
||||
# @ignore (only currently used to determine whether to fetch licenses offline or not, should
|
||||
# not normally need to be changed, and only applies to Enterprise customers)
|
||||
offlineMode: "0"
|
||||
# @ignore (only needs to be set in our cloud environment)
|
||||
accountPortalUrl: ""
|
||||
# @ignore (only needs to be set in our cloud environment)
|
||||
accountPortalApiKey: ""
|
||||
# -- Sets the domain attribute of the cookie that Budibase uses to store session information.
|
||||
# See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_where_cookies_are_sent>
|
||||
# for details on why you might want to set this.
|
||||
cookieDomain: ""
|
||||
# -- Set the `platformUrl` binding. You can also do this in Settings > Organisation if you are
|
||||
# self-hosting.
|
||||
platformUrl: ""
|
||||
# -- Whether or not to enable doing data migrations over the HTTP API. If this is set to "0",
|
||||
# migrations are run on startup. You shouldn't ordinarily need to change this.
|
||||
httpMigrations: "0"
|
||||
# -- Google OAuth settings. These can also be set in the Budibase UI, see
|
||||
# <https://docs.budibase.com/docs/sso-with-google> for details.
|
||||
google:
|
||||
# -- Client ID of your Google OAuth app.
|
||||
clientId: ""
|
||||
# -- Client secret of your Google OAuth app.
|
||||
secret: ""
|
||||
# -- The maximum number of iterations allows for an automation loop step. You can read more about
|
||||
# looping here: <https://docs.budibase.com/docs/looping>.
|
||||
automationMaxIterations: "200"
|
||||
|
||||
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
||||
# -- Create an internal API key, JWT secret, object store access key and
|
||||
# secret, and store them in a Kubernetes `Secret`.
|
||||
createSecrets: true
|
||||
|
||||
# if createSecrets is set to false, you can hard-code your secrets here
|
||||
# -- Used for encrypting API keys and environment variables when stored in the database.
|
||||
# You don't need to set this if `createSecrets` is true.
|
||||
apiEncryptionKey: ""
|
||||
# -- API key used for internal Budibase API calls. You don't need to set this
|
||||
# if `createSecrets` is true.
|
||||
internalApiKey: ""
|
||||
# -- Secret used for signing JWTs. You don't need to set this if `createSecrets` is true.
|
||||
jwtSecret: ""
|
||||
cdnUrl: ""
|
||||
# fallback values used during live rotation
|
||||
|
||||
# -- A fallback value for `internalApiKey`. If you're rotating your encryption key, you can
|
||||
# set this to the old value for the duration of the rotation.
|
||||
internalApiKeyFallback: ""
|
||||
# -- A fallback value for `jwtSecret`. If you're rotating your JWT secret, you can set this
|
||||
# to the old value for the duration of the rotation.
|
||||
jwtSecretFallback: ""
|
||||
|
||||
smtp:
|
||||
# -- Whether to enable SMTP or not.
|
||||
enabled: false
|
||||
|
||||
# globalAgentHttpProxy:
|
||||
# globalAgentHttpsProxy:
|
||||
# globalAgentNoProxy:
|
||||
# -- The hostname of your SMTP server.
|
||||
host: ""
|
||||
# -- The port of your SMTP server.
|
||||
port: "587"
|
||||
# -- The email address to use in the "From:" field of emails sent by Budibase.
|
||||
from: ""
|
||||
# -- The username to use when authenticating with your SMTP server.
|
||||
user: ""
|
||||
# -- The password to use when authenticating with your SMTP server.
|
||||
password: ""
|
||||
|
||||
services:
|
||||
budibaseVersion: latest
|
||||
# -- The DNS suffix to use for service discovery. You only need to change this
|
||||
# if you've configured your cluster to use a different DNS suffix.
|
||||
dns: cluster.local
|
||||
# tlsRejectUnauthorized: 0
|
||||
|
||||
proxy:
|
||||
# @ignore (you shouldn't need to change this)
|
||||
port: 10000
|
||||
# -- The number of proxy replicas to run.
|
||||
replicaCount: 1
|
||||
# @ignore (you should never need to change this)
|
||||
upstreams:
|
||||
apps: "http://app-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.apps.port }}"
|
||||
worker: "http://worker-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.worker.port }}"
|
||||
minio: "http://minio-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.objectStore.port }}"
|
||||
couchdb: "http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}"
|
||||
# -- The resources to use for proxy pods. See
|
||||
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
||||
# for more information on how to set these.
|
||||
resources: {}
|
||||
# -- Startup probe configuration for proxy pods. You shouldn't need to
|
||||
# change this, but if you want to you can find more information here:
|
||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||
# @default -- HTTP health checks.
|
||||
startupProbe:
|
||||
# @ignore
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 10000
|
||||
scheme: HTTP
|
||||
# @ignore
|
||||
failureThreshold: 30
|
||||
# @ignore
|
||||
periodSeconds: 3
|
||||
# -- Readiness probe configuration for proxy pods. You shouldn't need to
|
||||
# change this, but if you want to you can find more information here:
|
||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||
# @default -- HTTP health checks.
|
||||
readinessProbe:
|
||||
# @ignore
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 10000
|
||||
scheme: HTTP
|
||||
# @ignore
|
||||
periodSeconds: 3
|
||||
# @ignore
|
||||
failureThreshold: 1
|
||||
# -- Liveness probe configuration for proxy pods. You shouldn't need to
|
||||
# change this, but if you want to you can find more information here:
|
||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||
# @default -- HTTP health checks.
|
||||
livenessProbe:
|
||||
# @ignore
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 10000
|
||||
scheme: HTTP
|
||||
# @ignore
|
||||
failureThreshold: 3
|
||||
# @ignore
|
||||
periodSeconds: 5
|
||||
# annotations:
|
||||
# co.elastic.logs/module: nginx
|
||||
# co.elastic.logs/fileset.stdout: access
|
||||
# co.elastic.logs/fileset.stderr: error
|
||||
autoscaling:
|
||||
# -- Whether to enable horizontal pod autoscaling for the proxy service.
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 10
|
||||
# -- Target CPU utilization percentage for the proxy service. Note that
|
||||
# for autoscaling to work, you will need to have metrics-server
|
||||
# configured, and resources set for the proxy pods.
|
||||
targetCPUUtilizationPercentage: 80
|
||||
|
||||
apps:
|
||||
# @ignore (you shouldn't need to change this)
|
||||
port: 4002
|
||||
# -- The number of apps replicas to run.
|
||||
replicaCount: 1
|
||||
# -- The log level for the apps service.
|
||||
logLevel: info
|
||||
# -- Whether or not to log HTTP requests to the apps service.
|
||||
httpLogging: 1
|
||||
# -- The resources to use for apps pods. See
|
||||
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
||||
# for more information on how to set these.
|
||||
resources: {}
|
||||
# -- Startup probe configuration for apps pods. You shouldn't need to
|
||||
# change this, but if you want to you can find more information here:
|
||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||
# @default -- HTTP health checks.
|
||||
startupProbe:
|
||||
# @ignore
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 4002
|
||||
scheme: HTTP
|
||||
# @ignore
|
||||
failureThreshold: 30
|
||||
# @ignore
|
||||
periodSeconds: 3
|
||||
# -- Readiness probe configuration for apps pods. You shouldn't need to
|
||||
# change this, but if you want to you can find more information here:
|
||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||
# @default -- HTTP health checks.
|
||||
readinessProbe:
|
||||
# @ignore
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 4002
|
||||
scheme: HTTP
|
||||
# @ignore
|
||||
periodSeconds: 3
|
||||
# @ignore
|
||||
failureThreshold: 1
|
||||
# -- Liveness probe configuration for apps pods. You shouldn't need to
|
||||
# change this, but if you want to you can find more information here:
|
||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||
# @default -- HTTP health checks.
|
||||
livenessProbe:
|
||||
# @ignore
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 4002
|
||||
scheme: HTTP
|
||||
# @ignore
|
||||
failureThreshold: 3
|
||||
# @ignore
|
||||
periodSeconds: 5
|
||||
# nodeDebug: "" # set the value of NODE_DEBUG
|
||||
# annotations:
|
||||
# co.elastic.logs/multiline.type: pattern
|
||||
# co.elastic.logs/multiline.pattern: '^[[:space:]]'
|
||||
# co.elastic.logs/multiline.negate: false
|
||||
# co.elastic.logs/multiline.match: after
|
||||
autoscaling:
|
||||
# -- Whether to enable horizontal pod autoscaling for the apps service.
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 10
|
||||
# -- Target CPU utilization percentage for the apps service. Note that for
|
||||
# autoscaling to work, you will need to have metrics-server configured,
|
||||
# and resources set for the apps pods.
|
||||
targetCPUUtilizationPercentage: 80
|
||||
|
||||
worker:
|
||||
# @ignore (you shouldn't need to change this)
|
||||
port: 4003
|
||||
# -- The number of worker replicas to run.
|
||||
replicaCount: 1
|
||||
# -- The log level for the worker service.
|
||||
logLevel: info
|
||||
# -- Whether or not to log HTTP requests to the worker service.
|
||||
httpLogging: 1
|
||||
# -- The resources to use for worker pods. See
|
||||
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
||||
# for more information on how to set these.
|
||||
resources: {}
|
||||
# -- Startup probe configuration for worker pods. You shouldn't need to
|
||||
# change this, but if you want to you can find more information here:
|
||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||
# @default -- HTTP health checks.
|
||||
startupProbe:
|
||||
# @ignore
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 4003
|
||||
scheme: HTTP
|
||||
# @ignore
|
||||
failureThreshold: 30
|
||||
# @ignore
|
||||
periodSeconds: 3
|
||||
# -- Readiness probe configuration for worker pods. You shouldn't need to
|
||||
# change this, but if you want to you can find more information here:
|
||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||
# @default -- HTTP health checks.
|
||||
readinessProbe:
|
||||
# @ignore
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 4003
|
||||
scheme: HTTP
|
||||
# @ignore
|
||||
periodSeconds: 3
|
||||
# @ignore
|
||||
failureThreshold: 1
|
||||
# -- Liveness probe configuration for worker pods. You shouldn't need to
|
||||
# change this, but if you want to you can find more information here:
|
||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||
# @default -- HTTP health checks.
|
||||
livenessProbe:
|
||||
# @ignore
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 4003
|
||||
scheme: HTTP
|
||||
# @ignore
|
||||
failureThreshold: 3
|
||||
# @ignore
|
||||
periodSeconds: 5
|
||||
# annotations:
|
||||
# co.elastic.logs/multiline.type: pattern
|
||||
# co.elastic.logs/multiline.pattern: '^[[:space:]]'
|
||||
# co.elastic.logs/multiline.negate: false
|
||||
# co.elastic.logs/multiline.match: after
|
||||
autoscaling:
|
||||
# -- Whether to enable horizontal pod autoscaling for the worker service.
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 10
|
||||
# -- Target CPU utilization percentage for the worker service. Note that
|
||||
# for autoscaling to work, you will need to have metrics-server
|
||||
# configured, and resources set for the worker pods.
|
||||
targetCPUUtilizationPercentage: 80
|
||||
|
||||
couchdb:
|
||||
# -- Whether or not to spin up a CouchDB instance in your cluster. True by
|
||||
# default, and the configuration for the CouchDB instance is under the
|
||||
# `couchdb` key at the root of this file. You can see what options are
|
||||
# available to you by looking at the official CouchDB Helm chart:
|
||||
# <https://github.com/apache/couchdb-helm/tree/couchdb-4.3.0/couchdb>.
|
||||
enabled: true
|
||||
# url: "" # only change if pointing to existing couch server
|
||||
# user: "" # only change if pointing to existing couch server
|
||||
# password: "" # only change if pointing to existing couch server
|
||||
port: 5984
|
||||
backup:
|
||||
# -- Whether or not to enable periodic CouchDB backups. This works by replicating
|
||||
# to another CouchDB instance.
|
||||
enabled: false
|
||||
# target couchDB instance to back up to
|
||||
# -- Target couchDB instance to back up to, either a hostname or an IP address.
|
||||
target: ""
|
||||
# backup interval in seconds
|
||||
# -- Backup interval in seconds
|
||||
interval: ""
|
||||
# -- The resources to use for CouchDB backup pods. See
|
||||
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
||||
# for more information on how to set these.
|
||||
resources: {}
|
||||
|
||||
redis:
|
||||
enabled: true # disable if using external redis
|
||||
# -- Whether or not to deploy a Redis pod into your cluster.
|
||||
enabled: true
|
||||
# -- Port to expose Redis on.
|
||||
port: 6379
|
||||
# @ignore (you should leave this as 1, we don't support clustering Redis)
|
||||
replicaCount: 1
|
||||
url: "" # only change if pointing to existing redis cluster and enabled: false
|
||||
password: "budibase" # recommended to override if using built-in redis
|
||||
# -- If you choose to run Redis externally to this chart, you can specify the
|
||||
# connection details here.
|
||||
url: ""
|
||||
# -- The password to use when connecting to Redis. It's recommended that you change
|
||||
# this from the default if you're running Redis in-cluster.
|
||||
password: "budibase"
|
||||
# -- How much persistent storage to allocate to Redis.
|
||||
storage: 100Mi
|
||||
## If defined, storageClassName: <storageClass>
|
||||
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner.
|
||||
# -- If defined, storageClassName: <storageClass> If set to "-",
|
||||
# storageClassName: "", which disables dynamic provisioning If undefined
|
||||
# (the default) or set to null, no storageClassName spec is set, choosing
|
||||
# the default provisioner.
|
||||
storageClass: ""
|
||||
# -- The resources to use for Redis pods. See
|
||||
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
||||
# for more information on how to set these.
|
||||
resources: {}
|
||||
|
||||
objectStore:
|
||||
# Set to false if using another object store such as S3
|
||||
# -- Set to false if using another object store, such as S3. You will need
|
||||
# to set `services.objectStore.url` to point to your bucket if you do this.
|
||||
minio: true
|
||||
# -- Whether to enable the Minio web console or not. If you're exposing
|
||||
# Minio to the Internet (via a custom Ingress record, for example), you
|
||||
# should set this to false. If you're only exposing Minio to your cluster,
|
||||
# you can leave this as true.
|
||||
browser: true
|
||||
# @ignore
|
||||
port: 9000
|
||||
# @ignore (you should leave this as 1, we don't support clustering Minio)
|
||||
replicaCount: 1
|
||||
accessKey: "" # AWS_ACCESS_KEY if using S3 or existing minio access key
|
||||
secretKey: "" # AWS_SECRET_ACCESS_KEY if using S3 or existing minio secret
|
||||
region: "" # AWS_REGION if using S3 or existing minio secret
|
||||
url: "http://minio-service:9000" # only change if pointing to existing minio cluster or S3 and minio: false
|
||||
# -- AWS_ACCESS_KEY if using S3
|
||||
accessKey: ""
|
||||
# -- AWS_SECRET_ACCESS_KEY if using S3
|
||||
secretKey: ""
|
||||
# -- AWS_REGION if using S3
|
||||
region: ""
|
||||
# -- URL to use for object storage. Only change this if you're using an
|
||||
# external object store, such as S3. Remember to set `minio: false` if you
|
||||
# do this.
|
||||
url: "http://minio-service:9000"
|
||||
# -- How much storage to give Minio in its PersistentVolumeClaim.
|
||||
storage: 100Mi
|
||||
## If defined, storageClassName: <storageClass>
|
||||
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner.
|
||||
# -- If defined, storageClassName: <storageClass> If set to "-",
|
||||
# storageClassName: "", which disables dynamic provisioning If undefined
|
||||
# (the default) or set to null, no storageClassName spec is set, choosing
|
||||
# the default provisioner.
|
||||
storageClass: ""
|
||||
# -- The resources to use for Minio pods. See
|
||||
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
||||
# for more information on how to set these.
|
||||
resources: {}
|
||||
cloudfront:
|
||||
# Set the url of a distribution to enable cloudfront
|
||||
# -- Set the url of a distribution to enable cloudfront.
|
||||
cdn: ""
|
||||
# ID of public key stored in cloudfront
|
||||
# -- ID of public key stored in cloudfront.
|
||||
publicKeyId: ""
|
||||
# Base64 encoded private key for the above public key
|
||||
# -- Base64 encoded private key for the above public key.
|
||||
privateKey64: ""
|
||||
|
||||
# Override values in couchDB subchart
|
||||
# Override values in couchDB subchart. We're only specifying the values we're changing.
|
||||
# If you want to see all of the available values, see:
|
||||
# https://github.com/apache/couchdb-helm/tree/couchdb-4.3.0/couchdb
|
||||
couchdb:
|
||||
## clusterSize is the initial size of the CouchDB cluster.
|
||||
# -- The number of replicas to run in the CouchDB cluster. We set this to
|
||||
# 1 by default to make things simpler, but you can set it to 3 if you need
|
||||
# a high-availability CouchDB cluster.
|
||||
clusterSize: 1
|
||||
allowAdminParty: false
|
||||
|
||||
# Secret Management
|
||||
createAdminSecret: true
|
||||
|
||||
# adminUsername: budibase
|
||||
# adminPassword: budibase
|
||||
# adminHash: -pbkdf2-this_is_not_necessarily_secure_either
|
||||
# cookieAuthSecret: admin
|
||||
|
||||
## When enabled, will deploy a networkpolicy that allows CouchDB pods to
|
||||
## communicate with each other for clustering and ingress on port 5984
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
|
||||
# Use a service account
|
||||
serviceAccount:
|
||||
enabled: true
|
||||
create: true
|
||||
# name:
|
||||
# imagePullSecrets:
|
||||
# - name: myimagepullsecret
|
||||
|
||||
## The storage volume used by each Pod in the StatefulSet. If a
|
||||
## persistentVolume is not enabled, the Pods will use `emptyDir` ephemeral
|
||||
## local storage. Setting the storageClass attribute to "-" disables dynamic
|
||||
## provisioning of Persistent Volumes; leaving it unset will invoke the default
|
||||
## provisioner.
|
||||
persistentVolume:
|
||||
enabled: false
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
size: 10Gi
|
||||
storageClass: ""
|
||||
|
||||
## The CouchDB image
|
||||
# -- We use a custom CouchDB image for running Budibase and we don't support
|
||||
# using any other CouchDB image. You shouldn't change this, and if you do we
|
||||
# can't guarantee that Budibase will work.
|
||||
image:
|
||||
repository: couchdb
|
||||
tag: 3.1.1
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
## Experimental integration with Lucene-powered fulltext search
|
||||
enableSearch: true
|
||||
searchImage:
|
||||
repository: kocolosk/couchdb-search
|
||||
tag: 0.2.0
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
initImage:
|
||||
repository: busybox
|
||||
tag: latest
|
||||
# @ignore
|
||||
repository: budibase/couchdb
|
||||
# @ignore
|
||||
tag: v3.2.1
|
||||
# @ignore
|
||||
pullPolicy: Always
|
||||
|
||||
## CouchDB is happy to spin up cluster nodes in parallel, but if you encounter
|
||||
## problems you can try setting podManagementPolicy to the StatefulSet default
|
||||
## `OrderedReady`
|
||||
podManagementPolicy: Parallel
|
||||
# @ignore
|
||||
# This should remain false. We ship Clouseau ourselves as part of the
|
||||
# budibase/couchdb image, and it's not possible to disable it because it's a
|
||||
# core part of the Budibase experience.
|
||||
enableSearch: false
|
||||
|
||||
## Optional pod annotations
|
||||
annotations: {}
|
||||
|
||||
## Optional tolerations
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
service:
|
||||
# annotations:
|
||||
enabled: true
|
||||
type: ClusterIP
|
||||
externalPort: 5984
|
||||
|
||||
## An Ingress resource can provide name-based virtual hosting and TLS
|
||||
## termination among other things for CouchDB deployments which are accessed
|
||||
## from outside the Kubernetes cluster.
|
||||
## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
enabled: false
|
||||
hosts:
|
||||
- chart-example.local
|
||||
path: /
|
||||
annotations:
|
||||
[]
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
tls:
|
||||
# Secrets must be manually created in the namespace.
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
## Optional resource requests and limits for the CouchDB container
|
||||
## ref: http://kubernetes.io/docs/user-guide/compute-resources/
|
||||
resources:
|
||||
{}
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# limits:
|
||||
# cpu: 56
|
||||
# memory: 256Gi
|
||||
|
||||
## erlangFlags is a map that is passed to the Erlang VM as flags using the
|
||||
## ERL_FLAGS env. `name` and `setcookie` flags are minimally required to
|
||||
## establish connectivity between cluster nodes.
|
||||
## ref: http://erlang.org/doc/man/erl.html#init_flags
|
||||
erlangFlags:
|
||||
name: couchdb
|
||||
setcookie: monster
|
||||
|
||||
## couchdbConfig will override default CouchDB configuration settings.
|
||||
## The contents of this map are reformatted into a .ini file laid down
|
||||
## by a ConfigMap object.
|
||||
## ref: http://docs.couchdb.org/en/latest/config/index.html
|
||||
couchdbConfig:
|
||||
couchdb:
|
||||
uuid: budibase-couchdb # REQUIRED: Unique identifier for this CouchDB server instance
|
||||
# cluster:
|
||||
# q: 8 # Create 8 shards for each database
|
||||
chttpd:
|
||||
bind_address: any
|
||||
# chttpd.require_valid_user disables all the anonymous requests to the port
|
||||
# 5984 when is set to true.
|
||||
require_valid_user: false
|
||||
|
||||
# Kubernetes local cluster domain.
|
||||
# This is used to generate FQDNs for peers when joining the CouchDB cluster.
|
||||
dns:
|
||||
clusterDomainSuffix: cluster.local
|
||||
|
||||
## Configure liveness and readiness probe values
|
||||
## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes
|
||||
# FOR COUCHDB
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
initialDelaySeconds: 0
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
readinessProbe:
|
||||
failureThreshold: 3
|
||||
initialDelaySeconds: 0
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
# -- Unique identifier for this CouchDB server instance. You shouldn't need
|
||||
# to change this.
|
||||
uuid: budibase-couchdb
|
||||
|
|
|
@ -30,10 +30,18 @@ elif [[ "${TARGETBUILD}" = "single" ]]; then
|
|||
# mount, so we use that for all persistent data.
|
||||
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
||||
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
||||
elif [[ "${TARGETBUILD}" = "docker-compose" ]]; then
|
||||
# We remove the database_dir and view_index_dir settings from the local.ini
|
||||
# in docker-compose because it will default to /opt/couchdb/data which is what
|
||||
# our docker-compose was using prior to us switching to using our own CouchDB
|
||||
# image.
|
||||
sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini
|
||||
sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini
|
||||
sed -i "s#^dir=.*\$#dir=/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
|
||||
elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then
|
||||
# In Kubernetes the directory /opt/couchdb/data has a persistent volume
|
||||
# mount for storing database data.
|
||||
sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
|
||||
sed -i "s#^dir=.*\$#dir=/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
|
||||
|
||||
# We remove the database_dir and view_index_dir settings from the local.ini
|
||||
# in Kubernetes because it will default to /opt/couchdb/data which is what
|
||||
|
|
|
@ -57,7 +57,6 @@ services:
|
|||
depends_on:
|
||||
- redis-service
|
||||
- minio-service
|
||||
- couch-init
|
||||
|
||||
minio-service:
|
||||
restart: unless-stopped
|
||||
|
@ -70,7 +69,7 @@ services:
|
|||
MINIO_BROWSER: "off"
|
||||
command: server /data --console-address ":9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
test: "timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1"
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
@ -98,26 +97,15 @@ services:
|
|||
|
||||
couchdb-service:
|
||||
restart: unless-stopped
|
||||
image: ibmcom/couchdb3
|
||||
image: budibase/couchdb
|
||||
pull_policy: always
|
||||
environment:
|
||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||
- COUCHDB_USER=${COUCH_DB_USER}
|
||||
- TARGETBUILD=docker-compose
|
||||
volumes:
|
||||
- couchdb3_data:/opt/couchdb/data
|
||||
|
||||
couch-init:
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
PUT_CALL: "curl -u ${COUCH_DB_USER}:${COUCH_DB_PASSWORD} -X PUT couchdb-service:5984"
|
||||
depends_on:
|
||||
- couchdb-service
|
||||
command:
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
"sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;",
|
||||
]
|
||||
|
||||
redis-service:
|
||||
restart: unless-stopped
|
||||
image: redis
|
||||
|
|
|
@ -42,7 +42,7 @@ http {
|
|||
server {
|
||||
listen 10000 default_server;
|
||||
server_name _;
|
||||
client_max_body_size 1000m;
|
||||
client_max_body_size 50000m;
|
||||
ignore_invalid_headers off;
|
||||
proxy_buffering off;
|
||||
|
||||
|
|
|
@ -249,4 +249,30 @@ http {
|
|||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
||||
}
|
||||
|
||||
# From https://docs.datadoghq.com/integrations/nginx/?tab=kubernetes
|
||||
server {
|
||||
listen 81;
|
||||
server_name localhost;
|
||||
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
|
||||
location /nginx_status {
|
||||
# Choose your status module
|
||||
|
||||
# freely available with open source NGINX
|
||||
stub_status;
|
||||
|
||||
# for open source NGINX < version 1.7.5
|
||||
# stub_status on;
|
||||
|
||||
# available only with NGINX Plus
|
||||
# status;
|
||||
|
||||
# ensures the version information can be retrieved
|
||||
server_tokens on;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.13.30",
|
||||
"version": "2.13.35",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
"kill-port": "^1.6.1",
|
||||
"lerna": "7.1.1",
|
||||
"madge": "^6.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"nx": "16.4.3",
|
||||
"nx-cloud": "16.0.5",
|
||||
"prettier": "2.8.8",
|
||||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"svelte": "3.49.0",
|
||||
"svelte-eslint-parser": "^0.33.1",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "5.2.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/syncProPackage.js",
|
||||
|
@ -82,7 +82,8 @@
|
|||
"security:audit": "node scripts/audit.js",
|
||||
"postinstall": "husky install",
|
||||
"submodules:load": "git submodule init && git submodule update && yarn",
|
||||
"submodules:unload": "git submodule deinit --all && yarn"
|
||||
"submodules:unload": "git submodule deinit --all && yarn",
|
||||
"add-app-migration": "node scripts/add-app-migration.js --title"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
GoogleInnerConfig,
|
||||
OIDCInnerConfig,
|
||||
PlatformLogoutOpts,
|
||||
SessionCookie,
|
||||
SSOProviderType,
|
||||
} from "@budibase/types"
|
||||
import * as events from "../events"
|
||||
|
@ -44,7 +45,6 @@ export const buildAuthMiddleware = authenticated
|
|||
export const buildTenancyMiddleware = tenancy
|
||||
export const buildCsrfMiddleware = csrf
|
||||
export const passport = _passport
|
||||
export const jwt = require("jsonwebtoken")
|
||||
|
||||
// Strategies
|
||||
_passport.use(new LocalStrategy(local.options, local.authenticate))
|
||||
|
@ -191,10 +191,10 @@ export async function platformLogout(opts: PlatformLogoutOpts) {
|
|||
|
||||
if (!ctx) throw new Error("Koa context must be supplied to logout.")
|
||||
|
||||
const currentSession = getCookie(ctx, Cookie.Auth)
|
||||
const currentSession = getCookie<SessionCookie>(ctx, Cookie.Auth)
|
||||
let sessions = await getSessionsForUser(userId)
|
||||
|
||||
if (keepActiveSession) {
|
||||
if (currentSession && keepActiveSession) {
|
||||
sessions = sessions.filter(
|
||||
session => session.sessionId !== currentSession.sessionId
|
||||
)
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getGlobalDB, doInTenant } from "../context"
|
|||
import { decrypt } from "../security/encryption"
|
||||
import * as identity from "../context/identity"
|
||||
import env from "../environment"
|
||||
import { Ctx, EndpointMatcher } from "@budibase/types"
|
||||
import { Ctx, EndpointMatcher, SessionCookie } from "@budibase/types"
|
||||
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
||||
|
||||
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
|
||||
|
@ -98,7 +98,9 @@ export default function (
|
|||
// check the actual user is authenticated first, try header or cookie
|
||||
let headerToken = ctx.request.headers[Header.TOKEN]
|
||||
|
||||
const authCookie = getCookie(ctx, Cookie.Auth) || openJwt(headerToken)
|
||||
const authCookie =
|
||||
getCookie<SessionCookie>(ctx, Cookie.Auth) ||
|
||||
openJwt<SessionCookie>(headerToken)
|
||||
let apiKey = ctx.request.headers[Header.API_KEY]
|
||||
|
||||
if (!apiKey && ctx.request.headers[Header.AUTHORIZATION]) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Cookie } from "../../../constants"
|
|||
import * as configs from "../../../configs"
|
||||
import * as cache from "../../../cache"
|
||||
import * as utils from "../../../utils"
|
||||
import { UserCtx, SSOProfile } from "@budibase/types"
|
||||
import { UserCtx, SSOProfile, DatasourceAuthCookie } from "@budibase/types"
|
||||
import { ssoSaveUserNoOp } from "../sso/sso"
|
||||
|
||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||
|
@ -58,7 +58,14 @@ export async function postAuth(
|
|||
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
||||
|
||||
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
||||
const authStateCookie = utils.getCookie(ctx, Cookie.DatasourceAuth)
|
||||
const authStateCookie = utils.getCookie<{ appId: string }>(
|
||||
ctx,
|
||||
Cookie.DatasourceAuth
|
||||
)
|
||||
|
||||
if (!authStateCookie) {
|
||||
throw new Error("Unable to fetch datasource auth cookie")
|
||||
}
|
||||
|
||||
return passport.authenticate(
|
||||
new GoogleStrategy(
|
||||
|
|
|
@ -305,20 +305,33 @@ export async function retrieveDirectory(bucketName: string, path: string) {
|
|||
let writePath = join(budibaseTempDir(), v4())
|
||||
fs.mkdirSync(writePath)
|
||||
const objects = await listAllObjects(bucketName, path)
|
||||
let fullObjects = await Promise.all(
|
||||
objects.map(obj => retrieve(bucketName, obj.Key!))
|
||||
let streams = await Promise.all(
|
||||
objects.map(obj => getReadStream(bucketName, obj.Key!))
|
||||
)
|
||||
let count = 0
|
||||
const writePromises: Promise<Error>[] = []
|
||||
for (let obj of objects) {
|
||||
const filename = obj.Key!
|
||||
const data = fullObjects[count++]
|
||||
const stream = streams[count++]
|
||||
const possiblePath = filename.split("/")
|
||||
if (possiblePath.length > 1) {
|
||||
const dirs = possiblePath.slice(0, possiblePath.length - 1)
|
||||
fs.mkdirSync(join(writePath, ...dirs), { recursive: true })
|
||||
const dirs = possiblePath.slice(0, possiblePath.length - 1)
|
||||
const possibleDir = join(writePath, ...dirs)
|
||||
if (possiblePath.length > 1 && !fs.existsSync(possibleDir)) {
|
||||
fs.mkdirSync(possibleDir, { recursive: true })
|
||||
}
|
||||
fs.writeFileSync(join(writePath, ...possiblePath), data)
|
||||
const writeStream = fs.createWriteStream(join(writePath, ...possiblePath), {
|
||||
mode: 0o644,
|
||||
})
|
||||
stream.pipe(writeStream)
|
||||
writePromises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
stream.on("finish", resolve)
|
||||
stream.on("error", reject)
|
||||
writeStream.on("error", reject)
|
||||
})
|
||||
)
|
||||
}
|
||||
await Promise.all(writePromises)
|
||||
return writePath
|
||||
}
|
||||
|
||||
|
|
|
@ -3,4 +3,5 @@ export enum JobQueue {
|
|||
APP_BACKUP = "appBackupQueue",
|
||||
AUDIT_LOG = "auditLogQueue",
|
||||
SYSTEM_EVENT_QUEUE = "systemEventQueue",
|
||||
APP_MIGRATION = "appMigration",
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@ enum QueueEventType {
|
|||
APP_BACKUP_EVENT = "app-backup-event",
|
||||
AUDIT_LOG_EVENT = "audit-log-event",
|
||||
SYSTEM_EVENT = "system-event",
|
||||
APP_MIGRATION = "app-migration",
|
||||
}
|
||||
|
||||
const EventTypeMap: { [key in JobQueue]: QueueEventType } = {
|
||||
|
@ -94,6 +95,7 @@ const EventTypeMap: { [key in JobQueue]: QueueEventType } = {
|
|||
[JobQueue.APP_BACKUP]: QueueEventType.APP_BACKUP_EVENT,
|
||||
[JobQueue.AUDIT_LOG]: QueueEventType.AUDIT_LOG_EVENT,
|
||||
[JobQueue.SYSTEM_EVENT_QUEUE]: QueueEventType.SYSTEM_EVENT,
|
||||
[JobQueue.APP_MIGRATION]: QueueEventType.APP_MIGRATION,
|
||||
}
|
||||
|
||||
function logging(queue: Queue, jobQueue: JobQueue) {
|
||||
|
|
|
@ -109,7 +109,7 @@ export async function doWithLock<T>(
|
|||
): Promise<RedlockExecution<T>> {
|
||||
const redlock = await getClient(opts.type, opts.customOptions)
|
||||
let lock: Redlock.Lock | undefined
|
||||
let timeout: NodeJS.Timeout | undefined
|
||||
let timeout
|
||||
try {
|
||||
const name = getLockName(opts)
|
||||
|
||||
|
|
|
@ -73,6 +73,9 @@ export async function encryptFile(
|
|||
const outputFileName = `${filename}.enc`
|
||||
|
||||
const filePath = join(dir, filename)
|
||||
if (fs.lstatSync(filePath).isDirectory()) {
|
||||
throw new Error("Unable to encrypt directory")
|
||||
}
|
||||
const inputFile = fs.createReadStream(filePath)
|
||||
const outputFile = fs.createWriteStream(join(dir, outputFileName))
|
||||
|
||||
|
@ -110,6 +113,9 @@ export async function decryptFile(
|
|||
outputPath: string,
|
||||
secret: string
|
||||
) {
|
||||
if (fs.lstatSync(inputPath).isDirectory()) {
|
||||
throw new Error("Unable to encrypt directory")
|
||||
}
|
||||
const { salt, iv } = await getSaltAndIV(inputPath)
|
||||
const inputFile = fs.createReadStream(inputPath, {
|
||||
start: SALT_LENGTH + IV_LENGTH,
|
||||
|
|
|
@ -93,11 +93,19 @@ export const getTenantIDFromCtx = (
|
|||
// subdomain
|
||||
if (isAllowed(TenantResolutionStrategy.SUBDOMAIN)) {
|
||||
// e.g. budibase.app or local.com:10000
|
||||
const platformHost = new URL(getPlatformURL()).host.split(":")[0]
|
||||
let platformHost
|
||||
try {
|
||||
platformHost = new URL(getPlatformURL()).host.split(":")[0]
|
||||
} catch (err: any) {
|
||||
// if invalid URL, just don't try to process subdomain
|
||||
if (err.code !== "ERR_INVALID_URL") {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
// e.g. tenant.budibase.app or tenant.local.com
|
||||
const requestHost = ctx.host
|
||||
// parse the tenant id from the difference
|
||||
if (requestHost.includes(platformHost)) {
|
||||
if (platformHost && requestHost.includes(platformHost)) {
|
||||
const tenantId = requestHost.substring(
|
||||
0,
|
||||
requestHost.indexOf(`.${platformHost}`)
|
||||
|
|
|
@ -11,8 +11,7 @@ import {
|
|||
TenantResolutionStrategy,
|
||||
} from "@budibase/types"
|
||||
import type { SetOption } from "cookies"
|
||||
|
||||
const jwt = require("jsonwebtoken")
|
||||
import jwt, { Secret } from "jsonwebtoken"
|
||||
|
||||
const APP_PREFIX = DocumentType.APP + SEPARATOR
|
||||
const PROD_APP_PREFIX = "/app/"
|
||||
|
@ -60,10 +59,7 @@ export function isServingApp(ctx: Ctx) {
|
|||
return true
|
||||
}
|
||||
// prod app
|
||||
if (ctx.path.startsWith(PROD_APP_PREFIX)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return ctx.path.startsWith(PROD_APP_PREFIX)
|
||||
}
|
||||
|
||||
export function isServingBuilder(ctx: Ctx): boolean {
|
||||
|
@ -138,16 +134,16 @@ function parseAppIdFromUrl(url?: string) {
|
|||
* opens the contents of the specified encrypted JWT.
|
||||
* @return the contents of the token.
|
||||
*/
|
||||
export function openJwt(token: string) {
|
||||
export function openJwt<T>(token?: string): T | undefined {
|
||||
if (!token) {
|
||||
return token
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
return jwt.verify(token, env.JWT_SECRET)
|
||||
return jwt.verify(token, env.JWT_SECRET as Secret) as T
|
||||
} catch (e) {
|
||||
if (env.JWT_SECRET_FALLBACK) {
|
||||
// fallback to enable rotation
|
||||
return jwt.verify(token, env.JWT_SECRET_FALLBACK)
|
||||
return jwt.verify(token, env.JWT_SECRET_FALLBACK) as T
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
|
@ -159,13 +155,9 @@ export function isValidInternalAPIKey(apiKey: string) {
|
|||
return true
|
||||
}
|
||||
// fallback to enable rotation
|
||||
if (
|
||||
env.INTERNAL_API_KEY_FALLBACK &&
|
||||
env.INTERNAL_API_KEY_FALLBACK === apiKey
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return !!(
|
||||
env.INTERNAL_API_KEY_FALLBACK && env.INTERNAL_API_KEY_FALLBACK === apiKey
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -173,14 +165,14 @@ export function isValidInternalAPIKey(apiKey: string) {
|
|||
* @param ctx The request which is to be manipulated.
|
||||
* @param name The name of the cookie to get.
|
||||
*/
|
||||
export function getCookie(ctx: Ctx, name: string) {
|
||||
export function getCookie<T>(ctx: Ctx, name: string) {
|
||||
const cookie = ctx.cookies.get(name)
|
||||
|
||||
if (!cookie) {
|
||||
return cookie
|
||||
return undefined
|
||||
}
|
||||
|
||||
return openJwt(cookie)
|
||||
return openJwt<T>(cookie)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -197,7 +189,7 @@ export function setCookie(
|
|||
opts = { sign: true }
|
||||
) {
|
||||
if (value && opts && opts.sign) {
|
||||
value = jwt.sign(value, env.JWT_SECRET)
|
||||
value = jwt.sign(value, env.JWT_SECRET as Secret)
|
||||
}
|
||||
|
||||
const config: SetOption = {
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
<script>
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let name
|
||||
export let show = false
|
||||
export let initiallyShow = false
|
||||
export let collapsible = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let show = initiallyShow
|
||||
|
||||
const onHeaderClick = () => {
|
||||
if (!collapsible) {
|
||||
return
|
||||
}
|
||||
show = !show
|
||||
if (show) {
|
||||
dispatch("open")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
$: {
|
||||
if (selectedImage?.url) {
|
||||
selectedUrl = selectedImage?.url
|
||||
} else if (selectedImage) {
|
||||
} else if (selectedImage && isImage) {
|
||||
try {
|
||||
let reader = new FileReader()
|
||||
reader.readAsDataURL(selectedImage)
|
||||
|
|
|
@ -8,6 +8,7 @@ import { derived, get } from "svelte/store"
|
|||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { createHistoryStore } from "builderStore/store/history"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
|
@ -69,7 +70,14 @@ export const selectedComponent = derived(
|
|||
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||
return null
|
||||
}
|
||||
return findComponent($selectedScreen?.props, $store.selectedComponentId)
|
||||
const selected = findComponent(
|
||||
$selectedScreen?.props,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
|
||||
const clone = selected ? cloneDeep(selected) : selected
|
||||
store.actions.components.migrateSettings(clone)
|
||||
return clone
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -85,6 +85,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
selectedScreenId: null,
|
||||
selectedComponentId: null,
|
||||
selectedLayoutId: null,
|
||||
hoverComponentId: null,
|
||||
|
||||
// Client state
|
||||
selectedComponentInstance: null,
|
||||
|
@ -112,7 +113,7 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
let clone = cloneDeep(screen)
|
||||
const result = patchFn(clone)
|
||||
|
||||
// An explicit false result means skip this change
|
||||
if (result === false) {
|
||||
return
|
||||
}
|
||||
|
@ -601,6 +602,36 @@ export const getFrontendStore = () => {
|
|||
// Finally try an external table
|
||||
return validTables.find(table => table.sourceType === DB_TYPE_EXTERNAL)
|
||||
},
|
||||
migrateSettings: enrichedComponent => {
|
||||
const componentPrefix = "@budibase/standard-components"
|
||||
let migrated = false
|
||||
|
||||
if (enrichedComponent?._component == `${componentPrefix}/formblock`) {
|
||||
// Use default config if the 'buttons' prop has never been initialised
|
||||
if (!("buttons" in enrichedComponent)) {
|
||||
enrichedComponent["buttons"] =
|
||||
Utils.buildDynamicButtonConfig(enrichedComponent)
|
||||
migrated = true
|
||||
} else if (enrichedComponent["buttons"] == null) {
|
||||
// Ignore legacy config if 'buttons' has been reset by 'resetOn'
|
||||
const { _id, actionType, dataSource } = enrichedComponent
|
||||
enrichedComponent["buttons"] = Utils.buildDynamicButtonConfig({
|
||||
_id,
|
||||
actionType,
|
||||
dataSource,
|
||||
})
|
||||
migrated = true
|
||||
}
|
||||
|
||||
// Ensure existing Formblocks position their buttons at the top.
|
||||
if (!("buttonPosition" in enrichedComponent)) {
|
||||
enrichedComponent["buttonPosition"] = "top"
|
||||
migrated = true
|
||||
}
|
||||
}
|
||||
|
||||
return migrated
|
||||
},
|
||||
enrichEmptySettings: (component, opts) => {
|
||||
if (!component?._component) {
|
||||
return
|
||||
|
@ -672,7 +703,6 @@ export const getFrontendStore = () => {
|
|||
component[setting.key] = setting.defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// Validate non-empty settings
|
||||
else {
|
||||
if (setting.type === "dataProvider") {
|
||||
|
@ -722,6 +752,9 @@ export const getFrontendStore = () => {
|
|||
useDefaultValues: true,
|
||||
})
|
||||
|
||||
// Migrate nested component settings
|
||||
store.actions.components.migrateSettings(instance)
|
||||
|
||||
// Add any extra properties the component needs
|
||||
let extras = {}
|
||||
if (definition.hasChildren) {
|
||||
|
@ -845,7 +878,16 @@ export const getFrontendStore = () => {
|
|||
if (!component) {
|
||||
return false
|
||||
}
|
||||
return patchFn(component, screen)
|
||||
|
||||
// Mutates the fetched component with updates
|
||||
const patchResult = patchFn(component, screen)
|
||||
|
||||
// Mutates the component with any required settings updates
|
||||
const migrated = store.actions.components.migrateSettings(component)
|
||||
|
||||
// Returning an explicit false signifies that we should skip this
|
||||
// update. If we migrated something, ensure we never skip.
|
||||
return migrated ? null : patchResult
|
||||
}
|
||||
await store.actions.screens.patch(patchScreen, screenId)
|
||||
},
|
||||
|
@ -1247,9 +1289,13 @@ export const getFrontendStore = () => {
|
|||
const settings = getComponentSettings(component._component)
|
||||
const updatedSetting = settings.find(setting => setting.key === name)
|
||||
|
||||
const resetFields = settings.filter(
|
||||
setting => name === setting.resetOn
|
||||
)
|
||||
// Can be a single string or array of strings
|
||||
const resetFields = settings.filter(setting => {
|
||||
return (
|
||||
name === setting.resetOn ||
|
||||
(Array.isArray(setting.resetOn) && setting.resetOn.includes(name))
|
||||
)
|
||||
})
|
||||
resetFields?.forEach(setting => {
|
||||
component[setting.key] = null
|
||||
})
|
||||
|
@ -1271,6 +1317,7 @@ export const getFrontendStore = () => {
|
|||
})
|
||||
}
|
||||
component[name] = value
|
||||
return true
|
||||
}
|
||||
},
|
||||
requestEjectBlock: componentId => {
|
||||
|
@ -1278,7 +1325,6 @@ export const getFrontendStore = () => {
|
|||
},
|
||||
handleEjectBlock: async (componentId, ejectedDefinition) => {
|
||||
let nextSelectedComponentId
|
||||
|
||||
await store.actions.screens.patch(screen => {
|
||||
const block = findComponent(screen.props, componentId)
|
||||
const parent = findComponentParent(screen.props, componentId)
|
||||
|
|
|
@ -57,16 +57,11 @@
|
|||
}}
|
||||
class="buttons"
|
||||
>
|
||||
<Icon hoverable size="M" name="Play" />
|
||||
<Icon size="M" name="Play" />
|
||||
<div>Run test</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<Icon
|
||||
disabled={!$automationStore.testResults}
|
||||
hoverable
|
||||
size="M"
|
||||
name="Multiple"
|
||||
/>
|
||||
<Icon disabled={!$automationStore.testResults} size="M" name="Multiple" />
|
||||
<div
|
||||
class:disabled={!$automationStore.testResults}
|
||||
on:click={() => {
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
class:typing={typing && !automationNameError}
|
||||
class:typing-error={automationNameError}
|
||||
class="blockSection"
|
||||
on:click={() => dispatch("toggle")}
|
||||
>
|
||||
<div class="splitHeader">
|
||||
<div class="center-items">
|
||||
|
@ -138,7 +139,20 @@
|
|||
on:input={e => {
|
||||
automationName = e.target.value.trim()
|
||||
}}
|
||||
on:click={startTyping}
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
startTyping()
|
||||
}}
|
||||
on:keydown={async e => {
|
||||
if (e.key === "Enter") {
|
||||
typing = false
|
||||
if (automationNameError) {
|
||||
automationName = stepNames[block.id] || block?.name
|
||||
} else {
|
||||
await saveName()
|
||||
}
|
||||
}
|
||||
}}
|
||||
on:blur={async () => {
|
||||
typing = false
|
||||
if (automationNameError) {
|
||||
|
@ -168,7 +182,11 @@
|
|||
</StatusLight>
|
||||
</div>
|
||||
<Icon
|
||||
on:click={() => dispatch("toggle")}
|
||||
e.stopPropagation()
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
dispatch("toggle")
|
||||
}}
|
||||
hoverable
|
||||
name={open ? "ChevronUp" : "ChevronDown"}
|
||||
/>
|
||||
|
@ -195,7 +213,10 @@
|
|||
{/if}
|
||||
{#if !showTestStatus}
|
||||
<Icon
|
||||
on:click={() => dispatch("toggle")}
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
dispatch("toggle")
|
||||
}}
|
||||
hoverable
|
||||
name={open ? "ChevronUp" : "ChevronDown"}
|
||||
/>
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
<script>
|
||||
import {
|
||||
ModalContent,
|
||||
Tabs,
|
||||
Tab,
|
||||
TextArea,
|
||||
Label,
|
||||
notifications,
|
||||
ActionButton,
|
||||
} from "@budibase/bbui"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||
|
@ -55,50 +53,69 @@
|
|||
notifications.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
selectedValues = !selectedValues
|
||||
selectedJSON = !selectedJSON
|
||||
}
|
||||
let selectedValues = true
|
||||
let selectedJSON = false
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Add test data"
|
||||
confirmText="Test"
|
||||
size="M"
|
||||
confirmText="Run test"
|
||||
size="L"
|
||||
showConfirmButton={true}
|
||||
disabled={isError}
|
||||
onConfirm={testAutomation}
|
||||
cancelText="Cancel"
|
||||
>
|
||||
<Tabs selected="Form" quiet>
|
||||
<Tab icon="Form" title="Form">
|
||||
<div class="tab-content-padding">
|
||||
<AutomationBlockSetup
|
||||
{testData}
|
||||
{schemaProperties}
|
||||
isTestModal
|
||||
block={trigger}
|
||||
/>
|
||||
</div></Tab
|
||||
>
|
||||
<Tab icon="FileJson" title="JSON">
|
||||
<div class="tab-content-padding">
|
||||
<Label>JSON</Label>
|
||||
<div class="text-area-container">
|
||||
<TextArea
|
||||
value={JSON.stringify($selectedAutomation.testData, null, 2)}
|
||||
error={failedParse}
|
||||
on:change={e => parseTestJSON(e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div class="size">
|
||||
<div class="options">
|
||||
<ActionButton quiet selected={selectedValues} on:click={toggle}
|
||||
>Use values</ActionButton
|
||||
>
|
||||
<ActionButton quiet selected={selectedJSON} on:click={toggle}
|
||||
>Use JSON</ActionButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedValues}
|
||||
<div class="tab-content-padding">
|
||||
<AutomationBlockSetup
|
||||
{testData}
|
||||
{schemaProperties}
|
||||
isTestModal
|
||||
block={trigger}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedJSON}
|
||||
<div class="text-area-container">
|
||||
<TextArea
|
||||
value={JSON.stringify($selectedAutomation.testData, null, 2)}
|
||||
error={failedParse}
|
||||
on:change={e => parseTestJSON(e)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
.text-area-container :global(textarea) {
|
||||
min-height: 200px;
|
||||
height: 200px;
|
||||
min-height: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
padding: 0 var(--spacing-s);
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="title">
|
||||
<div class="title-text">
|
||||
<Icon name="MultipleCheck" />
|
||||
<div style="padding-left: var(--spacing-l)">Test Details</div>
|
||||
<div style="padding-left: var(--spacing-l); ">Test Details</div>
|
||||
</div>
|
||||
<div style="padding-right: var(--spacing-xl)">
|
||||
<Icon
|
||||
|
@ -40,6 +40,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
.title :global(h1) {
|
||||
|
|
|
@ -1,20 +1,44 @@
|
|||
<script>
|
||||
import AutomationList from "./AutomationList.svelte"
|
||||
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
||||
import { Modal, Button, Layout } from "@budibase/bbui"
|
||||
import { Modal, Icon } from "@budibase/bbui"
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
|
||||
export let modal
|
||||
export let webhookModal
|
||||
</script>
|
||||
|
||||
<Panel title="Automations" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Button cta on:click={modal.show}>Add automation</Button>
|
||||
</Layout>
|
||||
<Panel title="Automations" borderRight noHeaderBorder titleCSS={false}>
|
||||
<span class="panel-title-content" slot="panel-title-content">
|
||||
<div class="header">
|
||||
<div>Automations</div>
|
||||
<div on:click={modal.show} class="add-automation-button">
|
||||
<Icon name="Add" />
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<AutomationList />
|
||||
</Panel>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.add-automation-button {
|
||||
margin-left: 130px;
|
||||
color: var(--grey-7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-automation-button:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -149,7 +149,6 @@
|
|||
}
|
||||
const initialiseField = (field, savingColumn) => {
|
||||
isCreating = !field
|
||||
|
||||
if (field && !savingColumn) {
|
||||
editableColumn = cloneDeep(field)
|
||||
originalName = editableColumn.name ? editableColumn.name + "" : null
|
||||
|
@ -171,7 +170,8 @@
|
|||
relationshipPart2 = part2
|
||||
}
|
||||
}
|
||||
} else if (!savingColumn) {
|
||||
}
|
||||
if (!savingColumn && !originalName) {
|
||||
let highestNumber = 0
|
||||
Object.keys(table.schema).forEach(columnName => {
|
||||
const columnNumber = extractColumnNumber(columnName)
|
||||
|
@ -307,12 +307,6 @@
|
|||
dispatch("updatecolumns")
|
||||
gridDispatch("close-edit-column")
|
||||
|
||||
if (saveColumn.type === LINK_TYPE) {
|
||||
// Fetching the new tables
|
||||
tables.fetch()
|
||||
// Fetching the new relationships
|
||||
datasources.fetch()
|
||||
}
|
||||
if (originalName) {
|
||||
notifications.success("Column updated successfully")
|
||||
} else {
|
||||
|
@ -339,11 +333,6 @@
|
|||
confirmDeleteDialog.hide()
|
||||
dispatch("updatecolumns")
|
||||
gridDispatch("close-edit-column")
|
||||
|
||||
if (editableColumn.type === LINK_TYPE) {
|
||||
// Updating the relationships
|
||||
datasources.fetch()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error(`Error deleting column: ${error.message}`)
|
||||
|
@ -540,8 +529,16 @@
|
|||
<Layout noPadding gap="S">
|
||||
{#if mounted}
|
||||
<Input
|
||||
value={editableColumn.name}
|
||||
autofocus
|
||||
bind:value={editableColumn.name}
|
||||
on:input={e => {
|
||||
if (
|
||||
!uneditable &&
|
||||
!(linkEditDisabled && editableColumn.type === LINK_TYPE)
|
||||
) {
|
||||
editableColumn.name = e.target.value
|
||||
}
|
||||
}}
|
||||
disabled={uneditable ||
|
||||
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
|
||||
error={errors?.name}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
export let showTooltip = false
|
||||
export let selectedBy = null
|
||||
export let compact = false
|
||||
export let hovering = false
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -61,6 +62,7 @@
|
|||
|
||||
<div
|
||||
class="nav-item"
|
||||
class:hovering
|
||||
class:border
|
||||
class:selected
|
||||
class:withActions
|
||||
|
@ -71,6 +73,8 @@
|
|||
on:dragstart
|
||||
on:dragover
|
||||
on:drop
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:click={onClick}
|
||||
ondragover="return false"
|
||||
ondragenter="return false"
|
||||
|
@ -152,15 +156,17 @@
|
|||
--avatars-background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
.nav-item.selected {
|
||||
background-color: var(--spectrum-global-color-gray-300);
|
||||
background-color: var(--spectrum-global-color-gray-300) !important;
|
||||
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||
color: var(--ink);
|
||||
}
|
||||
.nav-item:hover {
|
||||
background-color: var(--spectrum-global-color-gray-300);
|
||||
.nav-item:hover,
|
||||
.hovering {
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.nav-item:hover .actions {
|
||||
.nav-item:hover .actions,
|
||||
.hovering .actions {
|
||||
visibility: visible;
|
||||
}
|
||||
.nav-item-content {
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
export let wide = false
|
||||
export let extraWide = false
|
||||
export let closeButtonIcon = "Close"
|
||||
|
||||
export let noHeaderBorder = false
|
||||
export let titleCSS = true
|
||||
$: customHeaderContent = $$slots["panel-header-content"]
|
||||
$: customTitleContent = $$slots["panel-title-content"]
|
||||
</script>
|
||||
|
@ -32,6 +33,7 @@
|
|||
class="header"
|
||||
class:custom={customHeaderContent}
|
||||
class:borderBottom={borderBottomHeader}
|
||||
class:noHeaderBorder
|
||||
>
|
||||
{#if showBackButton}
|
||||
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
||||
|
@ -41,7 +43,7 @@
|
|||
<Icon name={icon} />
|
||||
</AbsTooltip>
|
||||
{/if}
|
||||
<div class="title">
|
||||
<div class:title={titleCSS}>
|
||||
{#if customTitleContent}
|
||||
<slot name="panel-title-content" />
|
||||
{:else}
|
||||
|
@ -106,6 +108,10 @@
|
|||
padding: 0 var(--spacing-l);
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.noHeaderBorder {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
.header.borderBottom {
|
||||
border-bottom: var(--border-light);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,15 @@
|
|||
<div class="field-label">{item.label || item.field}</div>
|
||||
</div>
|
||||
<div class="list-item-right">
|
||||
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
|
||||
<Toggle
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
on:change={onToggle(item)}
|
||||
text=""
|
||||
value={item.active}
|
||||
thin
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
export let app
|
||||
export let published
|
||||
let includeInternalTablesRows = true
|
||||
let encypt = true
|
||||
let encrypt = true
|
||||
|
||||
let password = null
|
||||
const validation = createValidationStore()
|
||||
|
@ -27,9 +27,9 @@
|
|||
$: stepConfig = {
|
||||
[Step.CONFIG]: {
|
||||
title: published ? "Export published app" : "Export latest app",
|
||||
confirmText: encypt ? "Continue" : exportButtonText,
|
||||
confirmText: encrypt ? "Continue" : exportButtonText,
|
||||
onConfirm: () => {
|
||||
if (!encypt) {
|
||||
if (!encrypt) {
|
||||
exportApp()
|
||||
} else {
|
||||
currentStep = Step.SET_PASSWORD
|
||||
|
@ -46,7 +46,7 @@
|
|||
if (!$validation.valid) {
|
||||
return keepOpen
|
||||
}
|
||||
exportApp(password)
|
||||
await exportApp(password)
|
||||
},
|
||||
isValid: $validation.valid,
|
||||
},
|
||||
|
@ -109,13 +109,13 @@
|
|||
text="Export rows from internal tables"
|
||||
bind:value={includeInternalTablesRows}
|
||||
/>
|
||||
<Toggle text="Encrypt my export" bind:value={encypt} />
|
||||
<Toggle text="Encrypt my export" bind:value={encrypt} />
|
||||
</Body>
|
||||
{#if !encypt}
|
||||
<InlineAlert
|
||||
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."
|
||||
/>
|
||||
{/if}
|
||||
<InlineAlert
|
||||
header={encrypt
|
||||
? "Please note Budibase does not encrypt attachments during the export process to ensure efficient export of large attachments."
|
||||
: "Do not share your Budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."}
|
||||
/>
|
||||
{/if}
|
||||
{#if currentStep === Step.SET_PASSWORD}
|
||||
<Input
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
}
|
||||
|
||||
.setup {
|
||||
padding-top: var(--spectrum-global-dimension-size-200);
|
||||
padding-top: 9px;
|
||||
border-left: var(--border-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
const generalSettings = settings.filter(
|
||||
setting => !setting.section && setting.tag === tag
|
||||
)
|
||||
|
||||
const customSections = settings.filter(
|
||||
setting => setting.section && setting.tag === tag
|
||||
)
|
||||
|
@ -151,7 +152,7 @@
|
|||
{#if section.visible}
|
||||
<DetailSummary
|
||||
name={showSectionTitle ? section.name : ""}
|
||||
show={section.collapsed !== true}
|
||||
initiallyShow={section.collapsed !== true}
|
||||
>
|
||||
{#if section.info}
|
||||
<div class="section-info">
|
||||
|
|
|
@ -36,12 +36,14 @@
|
|||
|
||||
// Determine selected component ID
|
||||
$: selectedComponentId = $store.selectedComponentId
|
||||
$: hoverComponentId = $store.hoverComponentId
|
||||
|
||||
$: previewData = {
|
||||
appId: $store.appId,
|
||||
layout,
|
||||
screen,
|
||||
selectedComponentId,
|
||||
hoverComponentId,
|
||||
theme: $store.theme,
|
||||
customTheme: $store.customTheme,
|
||||
previewDevice: $store.previewDevice,
|
||||
|
@ -117,6 +119,8 @@
|
|||
error = event.error || "An unknown error occurred"
|
||||
} else if (type === "select-component" && data.id) {
|
||||
$store.selectedComponentId = data.id
|
||||
} else if (type === "hover-component" && data.id) {
|
||||
$store.hoverComponentId = data.id
|
||||
} else if (type === "update-prop") {
|
||||
await store.actions.components.updateSetting(data.prop, data.value)
|
||||
} else if (type === "update-styles") {
|
||||
|
|
|
@ -89,6 +89,17 @@
|
|||
}
|
||||
return findComponentPath($selectedComponent, component._id)?.length > 0
|
||||
}
|
||||
|
||||
const handleMouseover = componentId => {
|
||||
if ($store.hoverComponentId !== componentId) {
|
||||
$store.hoverComponentId = componentId
|
||||
}
|
||||
}
|
||||
const handleMouseout = componentId => {
|
||||
if ($store.hoverComponentId === componentId) {
|
||||
$store.hoverComponentId = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
|
@ -109,6 +120,9 @@
|
|||
on:dragover={dragover(component, index)}
|
||||
on:iconClick={() => toggleNodeOpen(component._id)}
|
||||
on:drop={onDrop}
|
||||
hovering={$store.hoverComponentId === component._id}
|
||||
on:mouseenter={() => handleMouseover(component._id)}
|
||||
on:mouseleave={() => handleMouseout(component._id)}
|
||||
text={getComponentText(component)}
|
||||
icon={getComponentIcon(component)}
|
||||
iconTooltip={getComponentName(component)}
|
||||
|
|
|
@ -32,6 +32,17 @@
|
|||
const handleScroll = e => {
|
||||
scrolling = e.target.scrollTop !== 0
|
||||
}
|
||||
|
||||
const handleMouseover = componentId => {
|
||||
if ($store.hoverComponentId !== componentId) {
|
||||
$store.hoverComponentId = componentId
|
||||
}
|
||||
}
|
||||
const handleMouseout = componentId => {
|
||||
if ($store.hoverComponentId === componentId) {
|
||||
$store.hoverComponentId = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="components">
|
||||
|
@ -57,6 +68,12 @@
|
|||
on:click={() => {
|
||||
$store.selectedComponentId = `${$store.selectedScreenId}-screen`
|
||||
}}
|
||||
hovering={$store.hoverComponentId ===
|
||||
`${$store.selectedScreenId}-screen`}
|
||||
on:mouseenter={() =>
|
||||
handleMouseover(`${$store.selectedScreenId}-screen`)}
|
||||
on:mouseleave={() =>
|
||||
handleMouseout(`${$store.selectedScreenId}-screen`)}
|
||||
id={`component-screen`}
|
||||
selectedBy={$userSelectedResourceMap[
|
||||
`${$store.selectedScreenId}-screen`
|
||||
|
@ -78,6 +95,12 @@
|
|||
on:click={() => {
|
||||
$store.selectedComponentId = `${$store.selectedScreenId}-navigation`
|
||||
}}
|
||||
hovering={$store.hoverComponentId ===
|
||||
`${$store.selectedScreenId}-navigation`}
|
||||
on:mouseenter={() =>
|
||||
handleMouseover(`${$store.selectedScreenId}-navigation`)}
|
||||
on:mouseleave={() =>
|
||||
handleMouseout(`${$store.selectedScreenId}-navigation`)}
|
||||
id={`component-nav`}
|
||||
selectedBy={$userSelectedResourceMap[
|
||||
`${$store.selectedScreenId}-navigation`
|
||||
|
|
|
@ -81,13 +81,21 @@ export function createTablesStore() {
|
|||
replaceTable(savedTable._id, savedTable)
|
||||
select(savedTable._id)
|
||||
// make sure tables up to date (related)
|
||||
let tableIdsToFetch = []
|
||||
let newTableIds = []
|
||||
for (let column of Object.values(updatedTable?.schema || {})) {
|
||||
if (column.type === FIELDS.LINK.type) {
|
||||
tableIdsToFetch.push(column.tableId)
|
||||
newTableIds.push(column.tableId)
|
||||
}
|
||||
}
|
||||
tableIdsToFetch = [...new Set(tableIdsToFetch)]
|
||||
|
||||
let oldTableIds = []
|
||||
for (let column of Object.values(oldTable?.schema || {})) {
|
||||
if (column.type === FIELDS.LINK.type) {
|
||||
oldTableIds.push(column.tableId)
|
||||
}
|
||||
}
|
||||
|
||||
const tableIdsToFetch = [...new Set([...newTableIds, ...oldTableIds])]
|
||||
// too many tables to fetch, just get all
|
||||
if (tableIdsToFetch.length > 3) {
|
||||
await fetch()
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
"pkg": "pkg . --out-path build --no-bytecode --public --public-packages \"*\" -C GZip",
|
||||
"build": "yarn prebuild && yarn rename && yarn tsc && yarn pkg && yarn postbuild",
|
||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
||||
"postbuild": "rm -rf prebuilds 2> /dev/null"
|
||||
"postbuild": "rm -rf prebuilds 2> /dev/null",
|
||||
"start": "ts-node ./src/index.ts"
|
||||
},
|
||||
"pkg": {
|
||||
"targets": [
|
||||
|
|
|
@ -6112,54 +6112,32 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "style",
|
||||
"type": "select",
|
||||
"label": "Button position",
|
||||
"key": "buttonPosition",
|
||||
"options": [
|
||||
{
|
||||
"label": "Bottom",
|
||||
"value": "bottom"
|
||||
},
|
||||
{
|
||||
"label": "Top",
|
||||
"value": "top"
|
||||
}
|
||||
],
|
||||
"defaultValue": "bottom"
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Buttons",
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "View",
|
||||
"invert": true
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "saveButtonLabel",
|
||||
"label": "Save button",
|
||||
"type": "buttonConfiguration",
|
||||
"key": "buttons",
|
||||
"nested": true,
|
||||
"defaultValue": "Save"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "deleteButtonLabel",
|
||||
"label": "Delete button",
|
||||
"nested": true,
|
||||
"defaultValue": "Delete",
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "Update"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Navigate after button press",
|
||||
"key": "actionUrl",
|
||||
"placeholder": "Choose a screen",
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "View",
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Hide notifications",
|
||||
"key": "notificationOverride",
|
||||
"defaultValue": false,
|
||||
"dependsOn": {
|
||||
"setting": "actionType",
|
||||
"value": "View",
|
||||
"invert": true
|
||||
}
|
||||
"resetOn": ["actionType", "dataSource"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
|
||||
export let title
|
||||
export let dataSource
|
||||
|
@ -33,6 +34,7 @@
|
|||
export let notificationOverride
|
||||
|
||||
const { fetchDatasourceSchema, API } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
const stateKey = `ID_${generate()}`
|
||||
|
||||
let formId
|
||||
|
@ -259,16 +261,25 @@
|
|||
name="Details form block"
|
||||
type="formblock"
|
||||
bind:id={detailsFormBlockId}
|
||||
context="form-edit"
|
||||
props={{
|
||||
dataSource,
|
||||
saveButtonLabel: sidePanelSaveLabel || "Save", //always show
|
||||
deleteButtonLabel: deleteLabel,
|
||||
buttonPosition: "top",
|
||||
buttons: Utils.buildDynamicButtonConfig({
|
||||
_id: $component.id + "-form-edit",
|
||||
showDeleteButton: deleteLabel !== "",
|
||||
showSaveButton: true,
|
||||
saveButtonLabel: sidePanelSaveLabel || "Save",
|
||||
deleteButtonLabel: deleteLabel,
|
||||
notificationOverride,
|
||||
actionType: "Update",
|
||||
dataSource,
|
||||
}),
|
||||
actionType: "Update",
|
||||
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
|
||||
fields: sidePanelFields || normalFields,
|
||||
title: editTitle,
|
||||
labelPosition: "left",
|
||||
notificationOverride,
|
||||
}}
|
||||
/>
|
||||
</BlockComponent>
|
||||
|
@ -284,16 +295,23 @@
|
|||
<BlockComponent
|
||||
name="New row form block"
|
||||
type="formblock"
|
||||
context="form-new"
|
||||
props={{
|
||||
dataSource,
|
||||
showSaveButton: true,
|
||||
showDeleteButton: false,
|
||||
saveButtonLabel: sidePanelSaveLabel || "Save", //always show
|
||||
buttonPosition: "top",
|
||||
buttons: Utils.buildDynamicButtonConfig({
|
||||
_id: $component.id + "-form-new",
|
||||
showDeleteButton: false,
|
||||
showSaveButton: true,
|
||||
saveButtonLabel: "Save",
|
||||
notificationOverride,
|
||||
actionType: "Create",
|
||||
dataSource,
|
||||
}),
|
||||
actionType: "Create",
|
||||
fields: sidePanelFields || normalFields,
|
||||
title: "Create Row",
|
||||
labelPosition: "left",
|
||||
notificationOverride,
|
||||
}}
|
||||
/>
|
||||
</BlockComponent>
|
||||
|
|
|
@ -4,28 +4,31 @@
|
|||
import Block from "components/Block.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import InnerFormBlock from "./InnerFormBlock.svelte"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
|
||||
export let actionType
|
||||
export let dataSource
|
||||
export let size
|
||||
export let disabled
|
||||
export let fields
|
||||
export let buttons
|
||||
export let buttonPosition
|
||||
|
||||
export let title
|
||||
export let description
|
||||
export let showDeleteButton
|
||||
export let showSaveButton
|
||||
export let saveButtonLabel
|
||||
export let deleteButtonLabel
|
||||
export let rowId
|
||||
export let actionUrl
|
||||
export let noRowsMessage
|
||||
export let notificationOverride
|
||||
|
||||
// Accommodate old config to ensure delete button does not reappear
|
||||
$: deleteLabel = showDeleteButton === false ? "" : deleteButtonLabel?.trim()
|
||||
$: saveLabel = showSaveButton === false ? "" : saveButtonLabel?.trim()
|
||||
// Legacy
|
||||
export let showDeleteButton
|
||||
export let showSaveButton
|
||||
export let saveButtonLabel
|
||||
export let deleteButtonLabel
|
||||
|
||||
const { fetchDatasourceSchema } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
const convertOldFieldFormat = fields => {
|
||||
if (!fields) {
|
||||
|
@ -98,11 +101,23 @@
|
|||
fields: fieldsOrDefault,
|
||||
title,
|
||||
description,
|
||||
saveButtonLabel: saveLabel,
|
||||
deleteButtonLabel: deleteLabel,
|
||||
schema,
|
||||
repeaterId,
|
||||
notificationOverride,
|
||||
buttons:
|
||||
buttons ||
|
||||
Utils.buildDynamicButtonConfig({
|
||||
_id: $component.id,
|
||||
showDeleteButton,
|
||||
showSaveButton,
|
||||
saveButtonLabel,
|
||||
deleteButtonLabel,
|
||||
notificationOverride,
|
||||
actionType,
|
||||
actionUrl,
|
||||
dataSource,
|
||||
}),
|
||||
buttonPosition: buttons ? buttonPosition : "top",
|
||||
}
|
||||
const fetchSchema = async () => {
|
||||
schema = (await fetchDatasourceSchema(dataSource)) || {}
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
<script>
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import Placeholder from "components/app/Placeholder.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
export let dataSource
|
||||
export let actionUrl
|
||||
export let actionType
|
||||
export let size
|
||||
export let disabled
|
||||
export let fields
|
||||
export let title
|
||||
export let description
|
||||
export let saveButtonLabel
|
||||
export let deleteButtonLabel
|
||||
export let buttons
|
||||
export let buttonPosition = "bottom"
|
||||
export let schema
|
||||
export let repeaterId
|
||||
export let notificationOverride
|
||||
|
||||
const FieldTypeToComponentMap = {
|
||||
string: "stringfield",
|
||||
|
@ -37,74 +33,7 @@
|
|||
|
||||
let formId
|
||||
|
||||
$: onSave = [
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
parameters: {
|
||||
componentId: formId,
|
||||
},
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
providerId: formId,
|
||||
tableId: dataSource?.resourceId,
|
||||
notificationOverride,
|
||||
},
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
},
|
||||
// Clear a create form once submitted
|
||||
...(actionType !== "Create"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
"##eventHandlerType": "Clear Form",
|
||||
parameters: {
|
||||
componentId: formId,
|
||||
},
|
||||
},
|
||||
]),
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: actionUrl,
|
||||
},
|
||||
},
|
||||
]
|
||||
$: onDelete = [
|
||||
{
|
||||
"##eventHandlerType": "Delete Row",
|
||||
parameters: {
|
||||
confirm: true,
|
||||
tableId: dataSource?.resourceId,
|
||||
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
|
||||
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
|
||||
notificationOverride,
|
||||
},
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: actionUrl,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
$: renderDeleteButton = deleteButtonLabel && actionType === "Update"
|
||||
$: renderSaveButton = saveButtonLabel && actionType !== "View"
|
||||
$: renderButtons = renderDeleteButton || renderSaveButton
|
||||
$: renderHeader = renderButtons || title
|
||||
$: renderHeader = buttons || title
|
||||
|
||||
const getComponentForField = field => {
|
||||
const fieldSchemaName = field.field || field.name
|
||||
|
@ -184,42 +113,14 @@
|
|||
props={{ text: title || "" }}
|
||||
order={0}
|
||||
/>
|
||||
{#if renderButtons}
|
||||
{#if buttonPosition == "top"}
|
||||
<BlockComponent
|
||||
type="container"
|
||||
type="buttongroup"
|
||||
props={{
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "center",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
buttons,
|
||||
}}
|
||||
order={1}
|
||||
>
|
||||
{#if renderDeleteButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: deleteButtonLabel,
|
||||
onClick: onDelete,
|
||||
quiet: true,
|
||||
type: "secondary",
|
||||
}}
|
||||
order={0}
|
||||
/>
|
||||
{/if}
|
||||
{#if renderSaveButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: saveButtonLabel,
|
||||
onClick: onSave,
|
||||
type: "cta",
|
||||
}}
|
||||
order={1}
|
||||
/>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
order={0}
|
||||
/>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
|
@ -245,6 +146,20 @@
|
|||
</BlockComponent>
|
||||
{/key}
|
||||
</BlockComponent>
|
||||
{#if buttonPosition === "bottom"}
|
||||
<BlockComponent
|
||||
type="buttongroup"
|
||||
props={{
|
||||
buttons,
|
||||
}}
|
||||
styles={{
|
||||
normal: {
|
||||
"margin-top": "16",
|
||||
},
|
||||
}}
|
||||
order={1}
|
||||
/>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
{:else}
|
||||
<Placeholder
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
import IndicatorSet from "./IndicatorSet.svelte"
|
||||
import { builderStore, dndIsDragging } from "stores"
|
||||
|
||||
let componentId
|
||||
|
||||
$: componentId = $builderStore.hoverComponentId
|
||||
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
||||
|
||||
const onMouseOver = e => {
|
||||
|
@ -24,12 +23,12 @@
|
|||
}
|
||||
|
||||
if (newId !== componentId) {
|
||||
componentId = newId
|
||||
builderStore.actions.hoverComponent(newId)
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseLeave = () => {
|
||||
componentId = null
|
||||
builderStore.actions.hoverComponent(null)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
|
|
@ -32,6 +32,7 @@ const loadBudibase = async () => {
|
|||
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
|
||||
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
||||
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
||||
hoverComponentId: window["##BUDIBASE_HOVER_COMPONENT_ID##"],
|
||||
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
||||
theme: window["##BUDIBASE_PREVIEW_THEME##"],
|
||||
customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
|
||||
|
|
|
@ -8,6 +8,7 @@ const createBuilderStore = () => {
|
|||
inBuilder: false,
|
||||
screen: null,
|
||||
selectedComponentId: null,
|
||||
hoverComponentId: null,
|
||||
editMode: false,
|
||||
previewId: null,
|
||||
theme: null,
|
||||
|
@ -23,6 +24,16 @@ const createBuilderStore = () => {
|
|||
}
|
||||
const store = writable(initialState)
|
||||
const actions = {
|
||||
hoverComponent: id => {
|
||||
if (id === get(store).hoverComponentId) {
|
||||
return
|
||||
}
|
||||
store.update(state => ({
|
||||
...state,
|
||||
hoverComponentId: id,
|
||||
}))
|
||||
eventStore.actions.dispatchEvent("hover-component", { id })
|
||||
},
|
||||
selectComponent: id => {
|
||||
if (id === get(store).selectedComponentId) {
|
||||
return
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
|
||||
/**
|
||||
* Utility to wrap an async function and ensure all invocations happen
|
||||
* sequentially.
|
||||
|
@ -106,3 +109,135 @@ export const domDebounce = callback => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the default FormBlock button configs per actionType
|
||||
* Parse any legacy button config and mirror its the outcome
|
||||
*
|
||||
* @param {any} props
|
||||
* */
|
||||
export const buildDynamicButtonConfig = props => {
|
||||
const {
|
||||
_id,
|
||||
actionType,
|
||||
dataSource,
|
||||
notificationOverride,
|
||||
actionUrl,
|
||||
showDeleteButton,
|
||||
deleteButtonLabel,
|
||||
showSaveButton,
|
||||
saveButtonLabel,
|
||||
} = props || {}
|
||||
|
||||
if (!_id) {
|
||||
console.log("MISSING ID")
|
||||
return
|
||||
}
|
||||
const formId = `${_id}-form`
|
||||
const repeaterId = `${_id}-repeater`
|
||||
const resourceId = dataSource?.resourceId
|
||||
|
||||
// Accommodate old config to ensure delete button does not reappear
|
||||
const deleteText = showDeleteButton === false ? "" : deleteButtonLabel?.trim()
|
||||
const saveText = showSaveButton === false ? "" : saveButtonLabel?.trim()
|
||||
|
||||
const onSave = [
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
parameters: {
|
||||
componentId: formId,
|
||||
},
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
providerId: formId,
|
||||
tableId: resourceId,
|
||||
notificationOverride,
|
||||
},
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
},
|
||||
// Clear a create form once submitted
|
||||
...(actionType !== "Create"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
"##eventHandlerType": "Clear Form",
|
||||
parameters: {
|
||||
componentId: formId,
|
||||
},
|
||||
},
|
||||
]),
|
||||
|
||||
...(actionUrl
|
||||
? [
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: actionUrl,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
const onDelete = [
|
||||
{
|
||||
"##eventHandlerType": "Delete Row",
|
||||
parameters: {
|
||||
confirm: true,
|
||||
tableId: resourceId,
|
||||
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
|
||||
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
|
||||
notificationOverride,
|
||||
},
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
},
|
||||
|
||||
...(actionUrl
|
||||
? [
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: actionUrl,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
const defaultButtons = []
|
||||
|
||||
if (["Update", "Create"].includes(actionType) && showSaveButton !== false) {
|
||||
defaultButtons.push({
|
||||
text: saveText || "Save",
|
||||
_id: Helpers.uuid(),
|
||||
_component: "@budibase/standard-components/button",
|
||||
onClick: onSave,
|
||||
type: "cta",
|
||||
})
|
||||
}
|
||||
|
||||
if (actionType == "Update" && showDeleteButton !== false) {
|
||||
defaultButtons.push({
|
||||
text: deleteText || "Delete",
|
||||
_id: Helpers.uuid(),
|
||||
_component: "@budibase/standard-components/button",
|
||||
onClick: onDelete,
|
||||
quiet: true,
|
||||
type: "secondary",
|
||||
})
|
||||
}
|
||||
|
||||
return defaultButtons
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Budibase server project
|
||||
|
||||
This project contains all the server specific logic required to run a Budibase app
|
||||
|
||||
## App migrations
|
||||
|
||||
A migration system has been created in order to modify existing apps when breaking changes are added. These migrations will run on the app startup (both from the client side or the builder side), blocking the access until they are correctly applied.
|
||||
|
||||
### Create a new migration
|
||||
|
||||
In order to add a new migration:
|
||||
|
||||
1. Run `yarn add-app-migration [title]`
|
||||
2. Write your code on the newly created file
|
|
@ -52,6 +52,7 @@ import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
|||
import sdk from "../../sdk"
|
||||
import { builderSocket } from "../../websockets"
|
||||
import { sdk as sharedCoreSDK } from "@budibase/shared-core"
|
||||
import * as appMigrations from "../../appMigrations"
|
||||
|
||||
// utility function, need to do away with this
|
||||
async function getLayouts() {
|
||||
|
@ -336,6 +337,12 @@ async function performAppCreate(ctx: UserCtx) {
|
|||
await createApp(appId)
|
||||
}
|
||||
|
||||
// Initialise the app migration version as the latest one
|
||||
await appMigrations.updateAppMigrationMetadata({
|
||||
appId,
|
||||
version: appMigrations.latestMigration,
|
||||
})
|
||||
|
||||
await cache.app.invalidateAppMetadata(appId, newApplication)
|
||||
return newApplication
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@ import { quotas } from "@budibase/pro"
|
|||
import { events, context, utils, constants } from "@budibase/backend-core"
|
||||
import sdk from "../../../sdk"
|
||||
import { QueryEvent } from "../../../threads/definitions"
|
||||
import { ConfigType, Query, UserCtx } from "@budibase/types"
|
||||
import { ConfigType, Query, UserCtx, SessionCookie } from "@budibase/types"
|
||||
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
||||
|
||||
const Runner = new Thread(ThreadType.QUERY, {
|
||||
|
@ -113,7 +113,7 @@ function getOAuthConfigCookieId(ctx: UserCtx) {
|
|||
}
|
||||
|
||||
function getAuthConfig(ctx: UserCtx) {
|
||||
const authCookie = utils.getCookie(ctx, constants.Cookie.Auth)
|
||||
const authCookie = utils.getCookie<SessionCookie>(ctx, constants.Cookie.Auth)
|
||||
let authConfigCtx: any = {}
|
||||
authConfigCtx["configId"] = getOAuthConfigCookieId(ctx)
|
||||
authConfigCtx["sessionId"] = authCookie ? authCookie.sessionId : null
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as linkRows from "../../../db/linkedRows"
|
|||
import { generateRowID, InternalTables } from "../../../db/utils"
|
||||
import * as userController from "../user"
|
||||
import {
|
||||
cleanupAttachments,
|
||||
AttachmentCleanup,
|
||||
inputProcessing,
|
||||
outputProcessing,
|
||||
} from "../../../utilities/rowProcessor"
|
||||
|
@ -79,7 +79,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
|||
table,
|
||||
})) as Row
|
||||
// check if any attachments removed
|
||||
await cleanupAttachments(table, { oldRow, row })
|
||||
await AttachmentCleanup.rowUpdate(table, { row, oldRow })
|
||||
|
||||
if (isUserTable) {
|
||||
// the row has been updated, need to put it into the ctx
|
||||
|
@ -119,7 +119,7 @@ export async function save(ctx: UserCtx) {
|
|||
throw { validation: validateResult.errors }
|
||||
}
|
||||
|
||||
// make sure link rows are up to date
|
||||
// make sure link rows are up-to-date
|
||||
row = (await linkRows.updateLinks({
|
||||
eventType: linkRows.EventType.ROW_SAVE,
|
||||
row,
|
||||
|
@ -165,7 +165,7 @@ export async function destroy(ctx: UserCtx) {
|
|||
tableId,
|
||||
})
|
||||
// remove any attachments that were on the row from object storage
|
||||
await cleanupAttachments(table, { row })
|
||||
await AttachmentCleanup.rowDelete(table, [row])
|
||||
// remove any static formula
|
||||
await updateRelatedFormula(table, row)
|
||||
|
||||
|
@ -216,7 +216,7 @@ export async function bulkDestroy(ctx: UserCtx) {
|
|||
await db.bulkDocs(processedRows.map(row => ({ ...row, _deleted: true })))
|
||||
}
|
||||
// remove any attachments that were on the rows from object storage
|
||||
await cleanupAttachments(table, { rows: processedRows })
|
||||
await AttachmentCleanup.rowDelete(table, processedRows)
|
||||
await updateRelatedFormula(table, processedRows)
|
||||
await Promise.all(updates)
|
||||
return { response: { ok: true }, rows: processedRows }
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
// Extract data from message
|
||||
const {
|
||||
selectedComponentId,
|
||||
hoverComponentId,
|
||||
layout,
|
||||
screen,
|
||||
appId,
|
||||
|
@ -81,6 +82,7 @@
|
|||
window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout
|
||||
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
|
||||
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
|
||||
window["##BUDIBASE_HOVER_COMPONENT_ID##"] = hoverComponentId
|
||||
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
|
||||
window["##BUDIBASE_PREVIEW_THEME##"] = theme
|
||||
window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"] = customTheme
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "../../../constants"
|
||||
import {
|
||||
inputProcessing,
|
||||
cleanupAttachments,
|
||||
AttachmentCleanup,
|
||||
} from "../../../utilities/rowProcessor"
|
||||
import { getViews, saveView } from "../view/utils"
|
||||
import viewTemplate from "../view/viewBuilder"
|
||||
|
@ -82,7 +82,10 @@ export async function checkForColumnUpdates(
|
|||
})
|
||||
|
||||
// cleanup any attachments from object storage for deleted attachment columns
|
||||
await cleanupAttachments(updatedTable, { oldTable, rows: rawRows })
|
||||
await AttachmentCleanup.tableUpdate(updatedTable, rawRows, {
|
||||
oldTable,
|
||||
rename: columnRename,
|
||||
})
|
||||
// Update views
|
||||
await checkForViewUpdates(updatedTable, deletedColumns, columnRename)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import currentApp from "../middleware/currentapp"
|
|||
import zlib from "zlib"
|
||||
import { mainRoutes, staticRoutes, publicRoutes } from "./routes"
|
||||
import { middleware as pro } from "@budibase/pro"
|
||||
import migrations from "../middleware/appMigrations"
|
||||
|
||||
export { shutdown } from "./routes/public"
|
||||
const compress = require("koa-compress")
|
||||
|
@ -47,6 +48,8 @@ router
|
|||
// @ts-ignore
|
||||
.use(currentApp)
|
||||
.use(auth.auditLog)
|
||||
// @ts-ignore
|
||||
.use(migrations)
|
||||
|
||||
// authenticated routes
|
||||
for (let route of mainRoutes) {
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import { Duration, cache, context, db, env } from "@budibase/backend-core"
|
||||
import { Database, DocumentType, Document } from "@budibase/types"
|
||||
|
||||
export interface AppMigrationDoc extends Document {
|
||||
version: string
|
||||
history: Record<string, { runAt: string }>
|
||||
}
|
||||
|
||||
const EXPIRY_SECONDS = Duration.fromDays(1).toSeconds()
|
||||
|
||||
async function getFromDB(appId: string) {
|
||||
return db.doWithDB(
|
||||
appId,
|
||||
(db: Database) => {
|
||||
return db.get<AppMigrationDoc>(DocumentType.APP_MIGRATION_METADATA)
|
||||
},
|
||||
{ skip_setup: true }
|
||||
)
|
||||
}
|
||||
|
||||
const getCacheKey = (appId: string) => `appmigrations_${env.VERSION}_${appId}`
|
||||
|
||||
export async function getAppMigrationVersion(appId: string): Promise<string> {
|
||||
const cacheKey = getCacheKey(appId)
|
||||
|
||||
let metadata: AppMigrationDoc | undefined = await cache.get(cacheKey)
|
||||
|
||||
// We don't want to cache in dev, in order to be able to tweak it
|
||||
if (metadata && !env.isDev()) {
|
||||
return metadata.version
|
||||
}
|
||||
|
||||
let version
|
||||
try {
|
||||
metadata = await getFromDB(appId)
|
||||
version = metadata.version
|
||||
} catch (err: any) {
|
||||
if (err.status !== 404) {
|
||||
throw err
|
||||
}
|
||||
|
||||
version = ""
|
||||
}
|
||||
|
||||
await cache.store(cacheKey, version, EXPIRY_SECONDS)
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
export async function updateAppMigrationMetadata({
|
||||
appId,
|
||||
version,
|
||||
}: {
|
||||
appId: string
|
||||
version: string
|
||||
}): Promise<void> {
|
||||
const db = context.getAppDB()
|
||||
|
||||
let appMigrationDoc: AppMigrationDoc
|
||||
|
||||
try {
|
||||
appMigrationDoc = await getFromDB(appId)
|
||||
} catch (err: any) {
|
||||
if (err.status !== 404) {
|
||||
throw err
|
||||
}
|
||||
|
||||
appMigrationDoc = {
|
||||
_id: DocumentType.APP_MIGRATION_METADATA,
|
||||
version: "",
|
||||
history: {},
|
||||
}
|
||||
await db.put(appMigrationDoc)
|
||||
appMigrationDoc = await getFromDB(appId)
|
||||
}
|
||||
|
||||
const updatedMigrationDoc: AppMigrationDoc = {
|
||||
...appMigrationDoc,
|
||||
version: version || "",
|
||||
history: {
|
||||
...appMigrationDoc.history,
|
||||
[version]: { runAt: new Date().toISOString() },
|
||||
},
|
||||
}
|
||||
await db.put(updatedMigrationDoc)
|
||||
|
||||
const cacheKey = getCacheKey(appId)
|
||||
|
||||
await cache.destroy(cacheKey)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import queue from "./queue"
|
||||
import { getAppMigrationVersion } from "./appMigrationMetadata"
|
||||
import { MIGRATIONS } from "./migrations"
|
||||
|
||||
export * from "./appMigrationMetadata"
|
||||
|
||||
export type AppMigration = {
|
||||
id: string
|
||||
func: () => Promise<void>
|
||||
}
|
||||
|
||||
export const latestMigration = MIGRATIONS.map(m => m.id)
|
||||
.sort()
|
||||
.reverse()[0]
|
||||
|
||||
const getTimestamp = (versionId: string) => versionId?.split("_")[0]
|
||||
|
||||
export async function checkMissingMigrations(appId: string) {
|
||||
const currentVersion = await getAppMigrationVersion(appId)
|
||||
|
||||
if (getTimestamp(currentVersion) < getTimestamp(latestMigration)) {
|
||||
await queue.add(
|
||||
{
|
||||
appId,
|
||||
},
|
||||
{
|
||||
jobId: `${appId}_${latestMigration}`,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// This file should never be manually modified, use `yarn add-app-migration` in order to add a new one
|
||||
|
||||
import { AppMigration } from "."
|
||||
|
||||
export const MIGRATIONS: AppMigration[] = [
|
||||
// Migrations will be executed sorted by id
|
||||
]
|
|
@ -0,0 +1,58 @@
|
|||
import { context, locks } from "@budibase/backend-core"
|
||||
import { LockName, LockType } from "@budibase/types"
|
||||
|
||||
import {
|
||||
getAppMigrationVersion,
|
||||
updateAppMigrationMetadata,
|
||||
} from "./appMigrationMetadata"
|
||||
import { AppMigration } from "."
|
||||
|
||||
export async function processMigrations(
|
||||
appId: string,
|
||||
migrations: AppMigration[]
|
||||
) {
|
||||
console.log(`Processing app migration for "${appId}"`)
|
||||
|
||||
await locks.doWithLock(
|
||||
{
|
||||
name: LockName.APP_MIGRATION,
|
||||
type: LockType.AUTO_EXTEND,
|
||||
resource: appId,
|
||||
},
|
||||
async () => {
|
||||
await context.doInAppMigrationContext(appId, async () => {
|
||||
let currentVersion = await getAppMigrationVersion(appId)
|
||||
|
||||
const pendingMigrations = migrations
|
||||
.filter(m => m.id > currentVersion)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
const migrationIds = migrations.map(m => m.id).sort()
|
||||
|
||||
let index = 0
|
||||
for (const { id, func } of pendingMigrations) {
|
||||
const expectedMigration =
|
||||
migrationIds[migrationIds.indexOf(currentVersion) + 1]
|
||||
|
||||
if (expectedMigration !== id) {
|
||||
throw `Migration ${id} could not run, update for "${id}" is running but ${expectedMigration} is expected`
|
||||
}
|
||||
|
||||
const counter = `(${++index}/${pendingMigrations.length})`
|
||||
console.info(`Running migration ${id}... ${counter}`, {
|
||||
migrationId: id,
|
||||
appId,
|
||||
})
|
||||
await func()
|
||||
await updateAppMigrationMetadata({
|
||||
appId,
|
||||
version: id,
|
||||
})
|
||||
currentVersion = id
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
console.log(`App migration for "${appId}" processed`)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { queue } from "@budibase/backend-core"
|
||||
import { Job } from "bull"
|
||||
import { MIGRATIONS } from "./migrations"
|
||||
import { processMigrations } from "./migrationsProcessor"
|
||||
|
||||
const appMigrationQueue = queue.createQueue(queue.JobQueue.APP_MIGRATION)
|
||||
appMigrationQueue.process(processMessage)
|
||||
|
||||
async function processMessage(job: Job) {
|
||||
const { appId } = job.data
|
||||
|
||||
await processMigrations(appId, MIGRATIONS)
|
||||
}
|
||||
|
||||
export default appMigrationQueue
|
|
@ -0,0 +1,25 @@
|
|||
import { context } from "@budibase/backend-core"
|
||||
import * as setup from "../../api/routes/tests/utilities"
|
||||
import { MIGRATIONS } from "../migrations"
|
||||
|
||||
describe("migration", () => {
|
||||
// These test is checking that each migration is "idempotent".
|
||||
// We should be able to rerun any migration, with any rerun not modifiying anything. The code should be aware that the migration already ran
|
||||
it("each migration can rerun safely", async () => {
|
||||
const config = setup.getConfig()
|
||||
await config.init()
|
||||
|
||||
await config.doInContext(config.getAppId(), async () => {
|
||||
const db = context.getAppDB()
|
||||
for (const migration of MIGRATIONS) {
|
||||
await migration.func()
|
||||
const docs = await db.allDocs({ include_docs: true })
|
||||
|
||||
await migration.func()
|
||||
const latestDocs = await db.allDocs({ include_docs: true })
|
||||
|
||||
expect(docs).toEqual(latestDocs)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,50 @@
|
|||
import * as setup from "../../api/routes/tests/utilities"
|
||||
import { processMigrations } from "../migrationsProcessor"
|
||||
import { getAppMigrationVersion } from "../appMigrationMetadata"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { AppMigration } from ".."
|
||||
|
||||
describe("migrationsProcessor", () => {
|
||||
it("running migrations will update the latest applied migration", async () => {
|
||||
const testMigrations: AppMigration[] = [
|
||||
{ id: "123", func: async () => {} },
|
||||
{ id: "124", func: async () => {} },
|
||||
{ id: "125", func: async () => {} },
|
||||
]
|
||||
|
||||
const config = setup.getConfig()
|
||||
await config.init()
|
||||
|
||||
const appId = config.getAppId()
|
||||
|
||||
await config.doInContext(appId, () =>
|
||||
processMigrations(appId, testMigrations)
|
||||
)
|
||||
|
||||
expect(
|
||||
await config.doInContext(appId, () => getAppMigrationVersion(appId))
|
||||
).toBe("125")
|
||||
})
|
||||
|
||||
it("no context can be initialised within a migration", async () => {
|
||||
const testMigrations: AppMigration[] = [
|
||||
{
|
||||
id: "123",
|
||||
func: async () => {
|
||||
await context.doInAppMigrationContext("any", () => {})
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const config = setup.getConfig()
|
||||
await config.init()
|
||||
|
||||
const appId = config.getAppId()
|
||||
|
||||
await expect(
|
||||
config.doInContext(appId, () => processMigrations(appId, testMigrations))
|
||||
).rejects.toThrowError(
|
||||
"The context cannot be changed, a migration is currently running"
|
||||
)
|
||||
})
|
||||
})
|
|
@ -59,6 +59,7 @@ const environment = {
|
|||
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
|
||||
PLUGINS_DIR: process.env.PLUGINS_DIR || "/plugins",
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
|
||||
// flags
|
||||
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
||||
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
||||
|
@ -87,6 +88,7 @@ const environment = {
|
|||
},
|
||||
TOP_LEVEL_PATH:
|
||||
process.env.TOP_LEVEL_PATH || process.env.SERVER_TOP_LEVEL_PATH,
|
||||
APP_MIGRATION_TIMEOUT: parseIntSafe(process.env.APP_MIGRATION_TIMEOUT),
|
||||
}
|
||||
|
||||
// threading can cause memory issues with node-ts in development
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import {
|
||||
getSqlQuery,
|
||||
buildExternalTableId,
|
||||
convertSqlType,
|
||||
generateColumnDefinition,
|
||||
finaliseExternalTables,
|
||||
SqlClient,
|
||||
checkExternalTables,
|
||||
|
@ -429,15 +429,12 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
|||
const hasDefault = def.COLUMN_DEFAULT
|
||||
const isAuto = !!autoColumns.find(col => col === name)
|
||||
const required = !!requiredColumns.find(col => col === name)
|
||||
schema[name] = {
|
||||
schema[name] = generateColumnDefinition({
|
||||
autocolumn: isAuto,
|
||||
name: name,
|
||||
constraints: {
|
||||
presence: required && !isAuto && !hasDefault,
|
||||
},
|
||||
...convertSqlType(def.DATA_TYPE),
|
||||
name,
|
||||
presence: required && !isAuto && !hasDefault,
|
||||
externalType: def.DATA_TYPE,
|
||||
}
|
||||
})
|
||||
}
|
||||
tables[tableName] = {
|
||||
_id: buildExternalTableId(datasourceId, tableName),
|
||||
|
|
|
@ -12,12 +12,13 @@ import {
|
|||
SourceName,
|
||||
Schema,
|
||||
TableSourceType,
|
||||
FieldType,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
getSqlQuery,
|
||||
SqlClient,
|
||||
buildExternalTableId,
|
||||
convertSqlType,
|
||||
generateColumnDefinition,
|
||||
finaliseExternalTables,
|
||||
checkExternalTables,
|
||||
} from "./utils"
|
||||
|
@ -305,16 +306,17 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
|||
(column.Extra === "auto_increment" ||
|
||||
column.Extra.toLowerCase().includes("generated"))
|
||||
const required = column.Null !== "YES"
|
||||
const constraints = {
|
||||
presence: required && !isAuto && !hasDefault,
|
||||
}
|
||||
schema[columnName] = {
|
||||
schema[columnName] = generateColumnDefinition({
|
||||
name: columnName,
|
||||
autocolumn: isAuto,
|
||||
constraints,
|
||||
...convertSqlType(column.Type),
|
||||
presence: required && !isAuto && !hasDefault,
|
||||
externalType: column.Type,
|
||||
}
|
||||
options: column.Type.startsWith("enum")
|
||||
? column.Type.substring(5, column.Type.length - 1)
|
||||
.split(",")
|
||||
.map(str => str.replace(/^'(.*)'$/, "$1"))
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
if (!tables[tableName]) {
|
||||
tables[tableName] = {
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
import {
|
||||
buildExternalTableId,
|
||||
checkExternalTables,
|
||||
convertSqlType,
|
||||
generateColumnDefinition,
|
||||
finaliseExternalTables,
|
||||
getSqlQuery,
|
||||
SqlClient,
|
||||
|
@ -250,14 +250,6 @@ class OracleIntegration extends Sql implements DatasourcePlus {
|
|||
)
|
||||
}
|
||||
|
||||
private internalConvertType(column: OracleColumn) {
|
||||
if (this.isBooleanType(column)) {
|
||||
return { type: FieldTypes.BOOLEAN }
|
||||
}
|
||||
|
||||
return convertSqlType(column.type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the tables from the oracle table and assigns them to the datasource.
|
||||
* @param datasourceId - datasourceId to fetch
|
||||
|
@ -302,13 +294,15 @@ class OracleIntegration extends Sql implements DatasourcePlus {
|
|||
const columnName = oracleColumn.name
|
||||
let fieldSchema = table.schema[columnName]
|
||||
if (!fieldSchema) {
|
||||
fieldSchema = {
|
||||
fieldSchema = generateColumnDefinition({
|
||||
autocolumn: OracleIntegration.isAutoColumn(oracleColumn),
|
||||
name: columnName,
|
||||
constraints: {
|
||||
presence: false,
|
||||
},
|
||||
...this.internalConvertType(oracleColumn),
|
||||
presence: false,
|
||||
externalType: oracleColumn.type,
|
||||
})
|
||||
|
||||
if (this.isBooleanType(oracleColumn)) {
|
||||
fieldSchema.type = FieldTypes.BOOLEAN
|
||||
}
|
||||
|
||||
table.schema[columnName] = fieldSchema
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import {
|
||||
getSqlQuery,
|
||||
buildExternalTableId,
|
||||
convertSqlType,
|
||||
generateColumnDefinition,
|
||||
finaliseExternalTables,
|
||||
SqlClient,
|
||||
checkExternalTables,
|
||||
|
@ -162,6 +162,14 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
|||
WHERE pg_namespace.nspname = '${this.config.schema}';
|
||||
`
|
||||
|
||||
ENUM_VALUES = () => `
|
||||
SELECT t.typname,
|
||||
e.enumlabel
|
||||
FROM pg_type t
|
||||
JOIN pg_enum e on t.oid = e.enumtypid
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace;
|
||||
`
|
||||
|
||||
constructor(config: PostgresConfig) {
|
||||
super(SqlClient.POSTGRES)
|
||||
this.config = config
|
||||
|
@ -303,6 +311,18 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
|||
|
||||
const tables: { [key: string]: Table } = {}
|
||||
|
||||
// Fetch enum values
|
||||
const enumsResponse = await this.client.query(this.ENUM_VALUES())
|
||||
const enumValues = enumsResponse.rows?.reduce((acc, row) => {
|
||||
if (!acc[row.typname]) {
|
||||
return {
|
||||
[row.typname]: [row.enumlabel],
|
||||
}
|
||||
}
|
||||
acc[row.typname].push(row.enumlabel)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
for (let column of columnsResponse.rows) {
|
||||
const tableName: string = column.table_name
|
||||
const columnName: string = column.column_name
|
||||
|
@ -333,16 +353,13 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
|||
column.is_generated && column.is_generated !== "NEVER"
|
||||
const isAuto: boolean = hasNextVal || identity || isGenerated
|
||||
const required = column.is_nullable === "NO"
|
||||
const constraints = {
|
||||
presence: required && !hasDefault && !isGenerated,
|
||||
}
|
||||
tables[tableName].schema[columnName] = {
|
||||
tables[tableName].schema[columnName] = generateColumnDefinition({
|
||||
autocolumn: isAuto,
|
||||
name: columnName,
|
||||
constraints,
|
||||
...convertSqlType(column.data_type),
|
||||
presence: required && !hasDefault && !isGenerated,
|
||||
externalType: column.data_type,
|
||||
}
|
||||
options: enumValues?.[column.udt_name],
|
||||
})
|
||||
}
|
||||
|
||||
let finalizedTables = finaliseExternalTables(tables, entities)
|
||||
|
|
|
@ -67,6 +67,10 @@ const SQL_BOOLEAN_TYPE_MAP = {
|
|||
tinyint: FieldType.BOOLEAN,
|
||||
}
|
||||
|
||||
const SQL_OPTIONS_TYPE_MAP = {
|
||||
"user-defined": FieldType.OPTIONS,
|
||||
}
|
||||
|
||||
const SQL_MISC_TYPE_MAP = {
|
||||
json: FieldType.JSON,
|
||||
bigint: FieldType.BIGINT,
|
||||
|
@ -78,6 +82,7 @@ const SQL_TYPE_MAP = {
|
|||
...SQL_STRING_TYPE_MAP,
|
||||
...SQL_BOOLEAN_TYPE_MAP,
|
||||
...SQL_MISC_TYPE_MAP,
|
||||
...SQL_OPTIONS_TYPE_MAP,
|
||||
}
|
||||
|
||||
export enum SqlClient {
|
||||
|
@ -178,25 +183,49 @@ export function breakRowIdField(_id: string | { _id: string }): any[] {
|
|||
}
|
||||
}
|
||||
|
||||
export function convertSqlType(type: string) {
|
||||
export function generateColumnDefinition(config: {
|
||||
externalType: string
|
||||
autocolumn: boolean
|
||||
name: string
|
||||
presence: boolean
|
||||
options?: string[]
|
||||
}) {
|
||||
let { externalType, autocolumn, name, presence, options } = config
|
||||
let foundType = FieldType.STRING
|
||||
const lcType = type.toLowerCase()
|
||||
const lowerCaseType = externalType.toLowerCase()
|
||||
let matchingTypes = []
|
||||
for (let [external, internal] of Object.entries(SQL_TYPE_MAP)) {
|
||||
if (lcType.includes(external)) {
|
||||
if (lowerCaseType.includes(external)) {
|
||||
matchingTypes.push({ external, internal })
|
||||
}
|
||||
}
|
||||
//Set the foundType based the longest match
|
||||
// Set the foundType based the longest match
|
||||
if (matchingTypes.length > 0) {
|
||||
foundType = matchingTypes.reduce((acc, val) => {
|
||||
return acc.external.length >= val.external.length ? acc : val
|
||||
}).internal
|
||||
}
|
||||
const schema: any = { type: foundType }
|
||||
|
||||
const constraints: {
|
||||
presence: boolean
|
||||
inclusion?: string[]
|
||||
} = {
|
||||
presence,
|
||||
}
|
||||
if (foundType === FieldType.OPTIONS) {
|
||||
constraints.inclusion = options
|
||||
}
|
||||
|
||||
const schema: any = {
|
||||
type: foundType,
|
||||
externalType,
|
||||
autocolumn,
|
||||
name,
|
||||
constraints,
|
||||
}
|
||||
if (foundType === FieldType.DATETIME) {
|
||||
schema.dateOnly = SQL_DATE_ONLY_TYPES.includes(lcType)
|
||||
schema.timeOnly = SQL_TIME_ONLY_TYPES.includes(lcType)
|
||||
schema.dateOnly = SQL_DATE_ONLY_TYPES.includes(lowerCaseType)
|
||||
schema.timeOnly = SQL_TIME_ONLY_TYPES.includes(lowerCaseType)
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import env from "./environment"
|
||||
import Koa, { ExtendableContext } from "koa"
|
||||
import Koa from "koa"
|
||||
import koaBody from "koa-body"
|
||||
import http from "http"
|
||||
import * as api from "./api"
|
||||
|
@ -27,6 +27,9 @@ export default function createKoaApp() {
|
|||
// @ts-ignore
|
||||
enableTypes: ["json", "form", "text"],
|
||||
parsedMethods: ["POST", "PUT", "PATCH", "DELETE"],
|
||||
formidable: {
|
||||
maxFileSize: parseInt(env.MAX_IMPORT_SIZE_MB || "100") * 1024 * 1024,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { UserCtx } from "@budibase/types"
|
||||
import { checkMissingMigrations } from "../appMigrations"
|
||||
|
||||
export default async (ctx: UserCtx, next: any) => {
|
||||
const { appId } = ctx
|
||||
|
||||
if (!appId) {
|
||||
return next()
|
||||
}
|
||||
|
||||
await checkMissingMigrations(appId)
|
||||
|
||||
return next()
|
||||
}
|
|
@ -5,6 +5,7 @@ import {
|
|||
tenancy,
|
||||
context,
|
||||
users,
|
||||
auth,
|
||||
} from "@budibase/backend-core"
|
||||
import { generateUserMetadataID, isDevAppID } from "../db/utils"
|
||||
import { getCachedSelf } from "../utilities/global"
|
||||
|
@ -69,28 +70,34 @@ export default async (ctx: UserCtx, next: any) => {
|
|||
return next()
|
||||
}
|
||||
|
||||
return context.doInAppContext(appId, async () => {
|
||||
// if the user not in the right tenant then make sure they have no permissions
|
||||
// need to judge this only based on the request app ID,
|
||||
if (
|
||||
env.MULTI_TENANCY &&
|
||||
ctx.user?._id &&
|
||||
requestAppId &&
|
||||
!tenancy.isUserInAppTenant(requestAppId, ctx.user)
|
||||
) {
|
||||
// don't error, simply remove the users rights (they are a public user)
|
||||
ctx.user = users.cleanseUserObject(ctx.user) as ContextUser
|
||||
ctx.isAuthenticated = false
|
||||
roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||
}
|
||||
const userId = ctx.user ? generateUserMetadataID(ctx.user._id!) : undefined
|
||||
|
||||
// if the user is not in the right tenant then make sure to wipe their cookie
|
||||
// also cleanse any information about them that has been allocated
|
||||
// this avoids apps making calls to say the worker which are cross tenant,
|
||||
// we simply remove the authentication
|
||||
if (
|
||||
env.MULTI_TENANCY &&
|
||||
userId &&
|
||||
requestAppId &&
|
||||
!tenancy.isUserInAppTenant(requestAppId, ctx.user)
|
||||
) {
|
||||
// clear out the user
|
||||
ctx.user = users.cleanseUserObject(ctx.user) as ContextUser
|
||||
ctx.isAuthenticated = false
|
||||
roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||
// remove the cookie, so future calls are public
|
||||
await auth.platformLogout({
|
||||
ctx,
|
||||
userId,
|
||||
})
|
||||
}
|
||||
|
||||
return context.doInAppContext(appId, async () => {
|
||||
ctx.appId = appId
|
||||
if (roleId) {
|
||||
ctx.roleId = roleId
|
||||
const globalId = ctx.user ? ctx.user._id : undefined
|
||||
const userId = ctx.user
|
||||
? generateUserMetadataID(ctx.user._id!)
|
||||
: undefined
|
||||
ctx.user = {
|
||||
...ctx.user!,
|
||||
// override userID with metadata one
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export const DB_EXPORT_FILE = "db.txt"
|
||||
export const GLOBAL_DB_EXPORT_FILE = "global.txt"
|
||||
export const STATIC_APP_FILES = ["manifest.json", "budibase-client.js"]
|
||||
export const ATTACHMENT_DIRECTORY = "attachments"
|
||||
|
|
|
@ -8,13 +8,15 @@ import {
|
|||
TABLE_ROW_PREFIX,
|
||||
USER_METDATA_PREFIX,
|
||||
} from "../../../db/utils"
|
||||
import { DB_EXPORT_FILE, STATIC_APP_FILES } from "./constants"
|
||||
import {
|
||||
DB_EXPORT_FILE,
|
||||
STATIC_APP_FILES,
|
||||
ATTACHMENT_DIRECTORY,
|
||||
} from "./constants"
|
||||
import fs from "fs"
|
||||
import { join } from "path"
|
||||
import env from "../../../environment"
|
||||
|
||||
const uuid = require("uuid/v4")
|
||||
|
||||
import { v4 as uuid } from "uuid"
|
||||
import tar from "tar"
|
||||
|
||||
const MemoryStream = require("memorystream")
|
||||
|
@ -30,12 +32,11 @@ export interface ExportOpts extends DBDumpOpts {
|
|||
encryptPassword?: string
|
||||
}
|
||||
|
||||
function tarFilesToTmp(tmpDir: string, files: string[]) {
|
||||
async function tarFilesToTmp(tmpDir: string, files: string[]) {
|
||||
const fileName = `${uuid()}.tar.gz`
|
||||
const exportFile = join(budibaseTempDir(), fileName)
|
||||
tar.create(
|
||||
await tar.create(
|
||||
{
|
||||
sync: true,
|
||||
gzip: true,
|
||||
file: exportFile,
|
||||
noDirRecurse: false,
|
||||
|
@ -150,19 +151,21 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|||
for (let file of fs.readdirSync(tmpPath)) {
|
||||
const path = join(tmpPath, file)
|
||||
|
||||
await encryption.encryptFile(
|
||||
{ dir: tmpPath, filename: file },
|
||||
config.encryptPassword
|
||||
)
|
||||
|
||||
fs.rmSync(path)
|
||||
// skip the attachments - too big to encrypt
|
||||
if (file !== ATTACHMENT_DIRECTORY) {
|
||||
await encryption.encryptFile(
|
||||
{ dir: tmpPath, filename: file },
|
||||
config.encryptPassword
|
||||
)
|
||||
fs.rmSync(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if tar requested, return where the tarball is
|
||||
if (config?.tar) {
|
||||
// now the tmpPath contains both the DB export and attachments, tar this
|
||||
const tarPath = tarFilesToTmp(tmpPath, fs.readdirSync(tmpPath))
|
||||
const tarPath = await tarFilesToTmp(tmpPath, fs.readdirSync(tmpPath))
|
||||
// cleanup the tmp export files as tarball returned
|
||||
fs.rmSync(tmpPath, { recursive: true, force: true })
|
||||
|
||||
|
|
|
@ -6,17 +6,20 @@ import {
|
|||
AutomationTriggerStepId,
|
||||
RowAttachment,
|
||||
} from "@budibase/types"
|
||||
import { getAutomationParams, TABLE_ROW_PREFIX } from "../../../db/utils"
|
||||
import { getAutomationParams } from "../../../db/utils"
|
||||
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
||||
import { DB_EXPORT_FILE, GLOBAL_DB_EXPORT_FILE } from "./constants"
|
||||
import {
|
||||
DB_EXPORT_FILE,
|
||||
GLOBAL_DB_EXPORT_FILE,
|
||||
ATTACHMENT_DIRECTORY,
|
||||
} from "./constants"
|
||||
import { downloadTemplate } from "../../../utilities/fileSystem"
|
||||
import { ObjectStoreBuckets } from "../../../constants"
|
||||
import { join } from "path"
|
||||
import fs from "fs"
|
||||
import sdk from "../../"
|
||||
|
||||
const uuid = require("uuid/v4")
|
||||
const tar = require("tar")
|
||||
import { v4 as uuid } from "uuid"
|
||||
import tar from "tar"
|
||||
|
||||
type TemplateType = {
|
||||
file?: {
|
||||
|
@ -114,12 +117,11 @@ async function getTemplateStream(template: TemplateType) {
|
|||
}
|
||||
}
|
||||
|
||||
export function untarFile(file: { path: string }) {
|
||||
export async function untarFile(file: { path: string }) {
|
||||
const tmpPath = join(budibaseTempDir(), uuid())
|
||||
fs.mkdirSync(tmpPath)
|
||||
// extract the tarball
|
||||
tar.extract({
|
||||
sync: true,
|
||||
await tar.extract({
|
||||
cwd: tmpPath,
|
||||
file: file.path,
|
||||
})
|
||||
|
@ -130,9 +132,11 @@ async function decryptFiles(path: string, password: string) {
|
|||
try {
|
||||
for (let file of fs.readdirSync(path)) {
|
||||
const inputPath = join(path, file)
|
||||
const outputPath = inputPath.replace(/\.enc$/, "")
|
||||
await encryption.decryptFile(inputPath, outputPath, password)
|
||||
fs.rmSync(inputPath)
|
||||
if (!inputPath.endsWith(ATTACHMENT_DIRECTORY)) {
|
||||
const outputPath = inputPath.replace(/\.enc$/, "")
|
||||
await encryption.decryptFile(inputPath, outputPath, password)
|
||||
fs.rmSync(inputPath)
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === "incorrect header check") {
|
||||
|
@ -162,7 +166,7 @@ export async function importApp(
|
|||
const isDirectory =
|
||||
template.file && fs.lstatSync(template.file.path).isDirectory()
|
||||
if (template.file && (isTar || isDirectory)) {
|
||||
const tmpPath = isTar ? untarFile(template.file) : template.file.path
|
||||
const tmpPath = isTar ? await untarFile(template.file) : template.file.path
|
||||
if (isTar && template.file.password) {
|
||||
await decryptFiles(tmpPath, template.file.password)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue