mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
36 Commits
doc/add-gi
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
796f76da46 | |||
d6e1ed4d1e | |||
1295b68d80 | |||
d0c50960ef | |||
85089a08e1 | |||
4053078d95 | |||
ccad684ab2 | |||
fd77708cad | |||
9aebd712d1 | |||
05f07b25ac | |||
60fb195706 | |||
c8109b4e84 | |||
1f2b0443cc | |||
dd1cabf9f6 | |||
8b781b925a | |||
ddcf5b576b | |||
7138b392f2 | |||
bfce1021fb | |||
93c0313b28 | |||
8cfc217519 | |||
d272c6217a | |||
2fe2ddd9fc | |||
e330ddd5ee | |||
7aba9c1a50 | |||
4cd8e0fa67 | |||
ea3d164ead | |||
df468e4865 | |||
66e96018c4 | |||
3b02eedca6 | |||
a55fe2b788 | |||
5d7a267f1d | |||
b16ab6f763 | |||
334a728259 | |||
4a3143e689 | |||
14810de054 | |||
8cfcbaa12c |
@ -10,8 +10,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
# packages: write
|
||||
# issues: write
|
||||
|
||||
jobs:
|
||||
cli-integration-tests:
|
||||
name: Run tests before deployment
|
||||
@ -26,6 +25,63 @@ jobs:
|
||||
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||
|
||||
npm-release:
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
working-directory: ./npm
|
||||
needs:
|
||||
- cli-integration-tests
|
||||
- goreleaser
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract version
|
||||
run: |
|
||||
VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//')
|
||||
echo "Version extracted: $VERSION"
|
||||
echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Print version
|
||||
run: echo ${{ env.CLI_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: ./npm/package-lock.json
|
||||
- name: Install dependencies
|
||||
working-directory: ${{ env.working-directory }}
|
||||
run: npm install --ignore-scripts
|
||||
|
||||
- name: Set NPM version
|
||||
working-directory: ${{ env.working-directory }}
|
||||
run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version
|
||||
|
||||
- name: Setup NPM
|
||||
working-directory: ${{ env.working-directory }}
|
||||
run: |
|
||||
echo 'registry="https://registry.npmjs.org/"' > ./.npmrc
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc
|
||||
|
||||
echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Pack NPM
|
||||
working-directory: ${{ env.working-directory }}
|
||||
run: npm pack
|
||||
|
||||
- name: Publish NPM
|
||||
working-directory: ${{ env.working-directory }}
|
||||
run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
goreleaser:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [cli-integration-tests]
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -71,3 +71,5 @@ frontend-build
|
||||
cli/infisical-merge
|
||||
cli/test/infisical-merge
|
||||
/backend/binary
|
||||
|
||||
/npm/bin
|
||||
|
@ -64,23 +64,25 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("caCertId").nullable();
|
||||
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
|
||||
});
|
||||
const hasCaCertIdColumn = await knex.schema.hasColumn(TableName.Certificate, "caCertId");
|
||||
if (!hasCaCertIdColumn) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("caCertId").nullable();
|
||||
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
await knex.raw(`
|
||||
UPDATE "${TableName.Certificate}" cert
|
||||
SET "caCertId" = (
|
||||
SELECT caCert.id
|
||||
FROM "${TableName.CertificateAuthorityCert}" caCert
|
||||
WHERE caCert."caId" = cert."caId"
|
||||
)
|
||||
`);
|
||||
)`);
|
||||
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("caCertId").notNullable().alter();
|
||||
});
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("caCertId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -840,4 +840,91 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/secrets-by-keys",
|
||||
config: {
|
||||
rateLimit: secretsLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||
keys: z.string().trim().transform(decodeURIComponent)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretPath: z.string().optional(),
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { secretPath, projectId, environment } = req.query;
|
||||
|
||||
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
|
||||
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
|
||||
|
||||
const { secrets } = await server.services.secret.getSecretsRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
keys
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secrets.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { secrets };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -361,6 +361,10 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
|
||||
}
|
||||
}
|
||||
|
||||
if (filters?.keys) {
|
||||
void bd.whereIn(`${TableName.SecretV2}.key`, filters.keys);
|
||||
}
|
||||
})
|
||||
.where((bd) => {
|
||||
void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });
|
||||
|
@ -518,7 +518,10 @@ export const expandSecretReferencesFactory = ({
|
||||
}
|
||||
|
||||
if (referencedSecretValue) {
|
||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referencedSecretValue);
|
||||
expandedValue = expandedValue.replaceAll(
|
||||
interpolationSyntax,
|
||||
() => referencedSecretValue // prevents special characters from triggering replacement patterns
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -150,9 +150,13 @@ export const secretV2BridgeServiceFactory = ({
|
||||
}
|
||||
});
|
||||
|
||||
if (referredSecrets.length !== references.length)
|
||||
if (
|
||||
referredSecrets.length !==
|
||||
new Set(references.map(({ secretKey, secretPath, environment }) => `${secretKey}.${secretPath}.${environment}`))
|
||||
.size // only count unique references
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message: `Referenced secret not found. Found only ${diff(
|
||||
message: `Referenced secret(s) not found: ${diff(
|
||||
references.map((el) => el.secretKey),
|
||||
referredSecrets.map((el) => el.key)
|
||||
).join(",")}`
|
||||
|
@ -33,6 +33,7 @@ export type TGetSecretsDTO = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
keys?: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretDTO = {
|
||||
@ -294,6 +295,7 @@ export type TFindSecretsByFolderIdsFilter = {
|
||||
search?: string;
|
||||
tagSlugs?: string[];
|
||||
includeTagsInSearch?: boolean;
|
||||
keys?: string[];
|
||||
};
|
||||
|
||||
export type TGetSecretsRawByFolderMappingsDTO = {
|
||||
|
@ -185,6 +185,7 @@ export type TGetSecretsRawDTO = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
keys?: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretRawDTO = {
|
||||
|
@ -532,7 +532,7 @@ func askForDomain() error {
|
||||
const (
|
||||
INFISICAL_CLOUD_US = "Infisical Cloud (US Region)"
|
||||
INFISICAL_CLOUD_EU = "Infisical Cloud (EU Region)"
|
||||
SELF_HOSTING = "Self-Hosting"
|
||||
SELF_HOSTING = "Self-Hosting or Dedicated Instance"
|
||||
ADD_NEW_DOMAIN = "Add a new domain"
|
||||
)
|
||||
|
||||
|
@ -69,4 +69,4 @@ volumes:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
infisical:
|
||||
infisical:
|
@ -9,7 +9,7 @@ You can use it across various environments, whether it's local development, CI/C
|
||||
## Installation
|
||||
|
||||
<Tabs>
|
||||
<Tab title="MacOS">
|
||||
<Tab title="MacOS">
|
||||
Use [brew](https://brew.sh/) package manager
|
||||
|
||||
```bash
|
||||
@ -21,9 +21,8 @@ You can use it across various environments, whether it's local development, CI/C
|
||||
```bash
|
||||
brew update && brew upgrade infisical
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Windows">
|
||||
</Tab>
|
||||
<Tab title="Windows">
|
||||
Use [Scoop](https://scoop.sh/) package manager
|
||||
|
||||
```bash
|
||||
@ -40,7 +39,20 @@ You can use it across various environments, whether it's local development, CI/C
|
||||
scoop update infisical
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tab>
|
||||
<Tab title="NPM">
|
||||
Use [NPM](https://www.npmjs.com/) package manager
|
||||
|
||||
```bash
|
||||
npm install -g @infisical/cli
|
||||
```
|
||||
|
||||
### Updates
|
||||
|
||||
```bash
|
||||
npm update -g @infisical/cli
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Alpine">
|
||||
Install prerequisite
|
||||
```bash
|
||||
|
@ -293,7 +293,10 @@
|
||||
},
|
||||
{
|
||||
"group": "Reference architectures",
|
||||
"pages": ["self-hosting/reference-architectures/aws-ecs"]
|
||||
"pages": [
|
||||
"self-hosting/reference-architectures/aws-ecs",
|
||||
"self-hosting/reference-architectures/linux-deployment-ha"
|
||||
]
|
||||
},
|
||||
"self-hosting/ee",
|
||||
"self-hosting/faq"
|
||||
|
@ -1,520 +0,0 @@
|
||||
---
|
||||
title: "Automatically deploy Infisical with High Availability"
|
||||
sidebarTitle: "High Availability"
|
||||
---
|
||||
|
||||
|
||||
# Self-Hosting Infisical with a native High Availability (HA) deployment
|
||||
|
||||
This page describes the Infisical architecture designed to provide high availability (HA) and how to deploy Infisical with high availability. The high availability deployment is designed to ensure that Infisical services are always available and can handle service failures gracefully, without causing service disruptions.
|
||||
|
||||
<Info>
|
||||
This deployment option is currently only available for Debian-based nodes (e.g., Ubuntu, Debian).
|
||||
We plan on adding support for other operating systems in the future.
|
||||
</Info>
|
||||
|
||||
## High availability architecture
|
||||
| Service | Nodes | Configuration | GCP | AWS |
|
||||
|----------------------------------|----------------|------------------------------|---------------|--------------|
|
||||
| External load balancer$^1$ | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge |
|
||||
| Internal load balancer$^2$ | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge |
|
||||
| Etcd cluster$^3$ | 3 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge |
|
||||
| PostgreSQL$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large |
|
||||
| Sentinel$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large |
|
||||
| Redis$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large |
|
||||
| Infisical Core | 3 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge |
|
||||
|
||||
**Footnotes:**
|
||||
1. External load balancer: If you wish to have multiple instances of the internal load balancer, you will need to use an external load balancer to distribute incoming traffic across multiple internal load balancers.
|
||||
Using multiple internal load balancers is recommended for high-traffic environments. In the following guide we will use a single internal load balancer, as external load balancing falls outside the scope of this guide.
|
||||
2. Internal load balancer: The internal load balancer (a HAProxy instance) is used to distribute incoming traffic across multiple Infisical Core instances, Postgres nodes, and Redis nodes. The internal load balancer exposes a set of ports _(80 for Infiscial, 5000 for Read/Write postgres, 5001 for Read-only postgres, and 6379 for Redis)_. Where these ports route to is determained by the internal load balancer based on the availability and health of the service nodes.
|
||||
The internal load balancer is only accessible from within the same network, and is not exposed to the public internet.
|
||||
3. Etcd cluster: Etcd is a distributed key-value store used to store and distribute data between the PostgreSQL nodes. Etcd is dependent on high disk I/O performance, therefore it is highly recommended to use highly performant SSD disks for the Etcd nodes, with _at least_ 80GB of disk space.
|
||||
4. The Redis and PostgreSQL nodes will automatically be configured for high availability and used in your Infisical Core instances. However, you can optionally choose to bring your own database (BYOD), and skip these nodes. See more on how to [provide your own databases](#provide-your-own-databases).
|
||||
|
||||
<Info>
|
||||
For all services that require multiple nodes, it is recommended to deploy them across multiple availability zones (AZs) to ensure high availability and fault tolerance. This will help prevent service disruptions in the event of an AZ failure.
|
||||
</Info>
|
||||
|
||||

|
||||
The image above shows how a high availability deployment of Infisical is structured. In this example, an external load balancer is used to distribute incoming traffic across multiple internal load balancers. The internal load balancers. The external load balancer isn't required, and it will require additional configuration to set up.
|
||||
|
||||
### Fault Tolerance
|
||||
This setup provides N+1 redundancy, meaning it can tolerate the failure of any single node without service interruption.
|
||||
|
||||
## Ansible
|
||||
### What is Ansible
|
||||
Ansible is an open-source automation tool that simplifies application deployment, configuration management, and task automation.
|
||||
At Infisical, we use Ansible to automate the deployment of Infisical services. The Ansible roles are designed to make it easy to deploy Infisical services in a high availability environment.
|
||||
|
||||
### Installing Ansible
|
||||
<Steps>
|
||||
<Step title="Install using the pipx Python package manager">
|
||||
```bash
|
||||
pipx install --include-deps ansible
|
||||
```
|
||||
</Step>
|
||||
<Step title="Verify the installation">
|
||||
```bash
|
||||
ansible --version
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
### Understanding Ansible Concepts
|
||||
|
||||
* Inventory _(inventory.ini)_: A file that lists your target hosts.
|
||||
* Playbook _(playbook.yml)_: YAML file containing a set of tasks to be executed on hosts.
|
||||
* Roles: Reusable units of organization for playbooks. Roles are used to group tasks together in a structured and reusable manner.
|
||||
|
||||
|
||||
### Basic Ansible Commands
|
||||
Running a playbook with with an invetory file:
|
||||
```bash
|
||||
ansible-playbook -i inventory.ini playbook.yml
|
||||
```
|
||||
|
||||
This is how you would run the playbook containing the roles for setting up Infisical in a high availability environment.
|
||||
|
||||
### Installing the Infisical High Availability Deployment Ansible Role
|
||||
The Infisical Ansible role is available on Ansible Galaxy. You can install the role by running the following command:
|
||||
```bash
|
||||
ansible-galaxy collection install infisical.infisical_core_ha_deployment
|
||||
```
|
||||
|
||||
|
||||
## Set up components
|
||||
1. External load balancer (optional, and not covered in this guide)
|
||||
2. [Configure Etcd cluster](#configure-etcd-cluster)
|
||||
3. [Configure PostgreSQL database](#configure-postgresql-database)
|
||||
4. [Configure Redis/Sentinel](#configure-redis-and-sentinel)
|
||||
5. [Configure Infisical Core](#configure-infisical-core)
|
||||
|
||||
|
||||
The servers start on the same 52.1.0.0/24 private network range, and can connect to each other freely on these addresses.
|
||||
|
||||
The following list includes descriptions of each server and its assigned IP:
|
||||
|
||||
52.1.0.1: External Load Balancer
|
||||
52.1.0.2: Internal Load Balancer
|
||||
52.1.0.3: Etcd 1
|
||||
52.1.0.4: Etcd 2
|
||||
52.1.0.5: Etcd 3
|
||||
52.1.0.6: PostgreSQL 1
|
||||
52.1.0.7: PostgreSQL 2
|
||||
52.1.0.8: PostgreSQL 3
|
||||
52.1.0.9: Redis 1
|
||||
52.1.0.10: Redis 2
|
||||
52.1.0.11: Redis 3
|
||||
52.1.0.12: Sentinel 1
|
||||
52.1.0.13: Sentinel 2
|
||||
52.1.0.14: Sentinel 3
|
||||
52.1.0.15: Infisical Core 1
|
||||
52.1.0.16: Infisical Core 2
|
||||
52.1.0.17: Infisical Core 3
|
||||
|
||||
|
||||
|
||||
### Configure Etcd cluster
|
||||
|
||||
Configuring the ETCD cluster is the first step in setting up a high availability deployment of Infisical.
|
||||
The ETCD cluster is used to store and distribute data between the PostgreSQL nodes. The ETCD cluster is a distributed key-value store that is highly available and fault-tolerant.
|
||||
|
||||
```yaml example.playbook.yml
|
||||
- hosts: all
|
||||
gather_facts: true
|
||||
|
||||
- name: Set up etcd cluster
|
||||
hosts: etcd
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: etcd
|
||||
```
|
||||
|
||||
```ini example.inventory.ini
|
||||
[etcd]
|
||||
etcd1 ansible_host=52.1.0.3
|
||||
etcd2 ansible_host=52.1.0.4
|
||||
etcd3 ansible_host=52.1.0.5
|
||||
|
||||
[etcd:vars]
|
||||
ansible_user=ubuntu
|
||||
ansible_ssh_private_key_file=./ssh-key.pem
|
||||
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
|
||||
```
|
||||
|
||||
### Configure PostgreSQL database
|
||||
|
||||
The Postgres role takes a set of parameters that are used to configure your PostgreSQL database.
|
||||
|
||||
Make sure to set the following variables in your playbook.yml file:
|
||||
- `postgres_super_user_password`: The password for the 'postgres' database user.
|
||||
- `postgres_db_name`: The name of the database that will be created on the leader node and replicated to the secondary nodes.
|
||||
- `postgres_user`: The name of the user that will be created on the leader node and replicated to the secondary nodes.
|
||||
- `postgres_user_password`: The password for the user that will be created on the leader node and replicated to the secondary nodes.
|
||||
- `etcd_hosts`: The list of etcd hosts that the PostgreSQL nodes will use to communicate with etcd. By default you want to keep this value set to `"{{ groups['etcd'] }}"`
|
||||
|
||||
```yaml example.playbook.yml
|
||||
- hosts: all
|
||||
gather_facts: true
|
||||
|
||||
- name: Set up PostgreSQL with Patroni
|
||||
hosts: postgres
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: postgres
|
||||
vars:
|
||||
postgres_super_user_password: "your-super-user-password"
|
||||
postgres_user: infisical-user
|
||||
postgres_user_password: "your-password"
|
||||
postgres_db_name: infisical-db
|
||||
|
||||
etcd_hosts: "{{ groups['etcd'] }}"
|
||||
```
|
||||
|
||||
```ini example.inventory.ini
|
||||
[postgres]
|
||||
postgres1 ansible_host=52.1.0.6
|
||||
postgres2 ansible_host=52.1.0.7
|
||||
postgres3 ansible_host=52.1.0.8
|
||||
```
|
||||
|
||||
### Configure Redis and Sentinel
|
||||
|
||||
The Redis role takes a single variable as input, which is the redis password.
|
||||
The Sentinel and Redis hosts will run the same role, therefore we are running the task for both the sentinel and redis hosts, `hosts: redis:sentinel`.
|
||||
|
||||
- `redis_password`: The password that will be set for the Redis instance.
|
||||
|
||||
```yaml example.playbook.yml
|
||||
- hosts: all
|
||||
gather_facts: true
|
||||
|
||||
- name: Setup Redis and Sentinel
|
||||
hosts: redis:sentinel
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: redis
|
||||
vars:
|
||||
redis_password: "REDIS_PASSWORD"
|
||||
```
|
||||
|
||||
```ini example.inventory.ini
|
||||
[redis]
|
||||
redis1 ansible_host=52.1.0.9
|
||||
redis2 ansible_host=52.1.0.10
|
||||
redis3 ansible_host=52.1.0.11
|
||||
|
||||
[sentinel]
|
||||
sentinel1 ansible_host=52.1.0.12
|
||||
sentinel2 ansible_host=52.1.0.13
|
||||
sentinel3 ansible_host=52.1.0.14
|
||||
```
|
||||
|
||||
### Configure Internal Load Balancer
|
||||
|
||||
The internal load balancer used is HAProxy. HAProxy will expose a set of ports as listed below. Each port will route to a different service based on the availability and health of the service nodes.
|
||||
|
||||
- Port 80: Infisical Core
|
||||
- Port 5000: Read/Write PostgreSQL
|
||||
- Port 5001: Read-only PostgreSQL
|
||||
- Port 6379: Redis
|
||||
- Port 7000: HAProxy monitoring
|
||||
These ports will need to be exposed on your network to become accessible from the outside world.
|
||||
|
||||
The HAProxy configuration file is generated by the Infisical Core role, and is located at `/etc/haproxy/haproxy.cfg` on your internal load balancer node.
|
||||
|
||||
The HAProxy setup comes with a monitoring panel. You have to set the username/password combination for the monitoring panel by setting the `stats_user` and `stats_password` variables in the HAProxy role.
|
||||
|
||||
|
||||
Once the HAProxy role has fully executed, you can monitor your HA setup by navigating to `http://52.1.0.2:7000/haproxy?stats` in your browser.
|
||||
|
||||
```ini example.inventory.ini
|
||||
[haproxy]
|
||||
internal_lb ansible_host=52.1.0.2
|
||||
```
|
||||
|
||||
```yaml example.playbook.yml
|
||||
- name: Set up HAProxy
|
||||
hosts: haproxy
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: haproxy
|
||||
vars:
|
||||
stats_user: "stats-username"
|
||||
stats_password: "stats-password!"
|
||||
|
||||
postgres_servers: "{{ groups['postgres'] }}"
|
||||
infisical_servers: "{{ groups['infisical'] }}"
|
||||
redis_servers: "{{ groups['redis'] }}"
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Configure Infisical Core
|
||||
|
||||
The Infisical Core role will set up your actual Infisical instances.
|
||||
|
||||
The `env_vars` variable is used to set the environment variables that Infisical will use. The minimum required environment variables are `ENCRYPTION_KEY` and `AUTH_SECRET`. You can find a list of all available environment variables [here](/docs/self-hosting/configuration/envars#general-platform).
|
||||
The `DB_CONNECTION_URI` and `REDIS_URL` variables will automatically be set if you're running the full playbook. However, you can choose to set them yourself, and skip the Postgres, etcd, redis/sentinel roles entirely.
|
||||
|
||||
<Info>
|
||||
If you later need to add new environment varibles to your Infisical deployments, it's important you add the variables to **all** your Infisical nodes.<br/>
|
||||
You can find the environment file for Infisical at `/etc/infisical/environment`.<br/>
|
||||
After editing the environment file, you need to reload the Infisical service by doing `systemctl restart infisical`.
|
||||
</Info>
|
||||
|
||||
```yaml example.playbook.yml
|
||||
- hosts: all
|
||||
gather_facts: true
|
||||
|
||||
- name: Setup Infisical
|
||||
hosts: infisical
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: infisical
|
||||
env_vars:
|
||||
ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16
|
||||
AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32
|
||||
```
|
||||
|
||||
```ini example.inventory.ini
|
||||
[infisical]
|
||||
infisical1 ansible_host=52.1.0.15
|
||||
infisical2 ansible_host=52.1.0.16
|
||||
infisical3 ansible_host=52.1.0.17
|
||||
```
|
||||
|
||||
## Provide your own databases
|
||||
Bringing your own database is an option using the Infisical Core deployment role.
|
||||
By bringing your own database, you're able to skip the Etcd, Postgres, and Redis/Sentinel roles entirely.
|
||||
|
||||
To bring your own database, you need to set the `DB_CONNECTION_URI` and `REDIS_URL` environment variables in the Infisical Core role.
|
||||
|
||||
```yaml example.playbook.yml
|
||||
- hosts: all
|
||||
gather_facts: true
|
||||
|
||||
- name: Setup Infisical
|
||||
hosts: infisical
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: infisical
|
||||
env_vars:
|
||||
ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16
|
||||
AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32
|
||||
DB_CONNECTION_URI: "postgres://user:password@localhost:5432/infisical"
|
||||
REDIS_URL: "redis://localhost:6379"
|
||||
```
|
||||
|
||||
```ini example.inventory.ini
|
||||
[infisical]
|
||||
infisical1 ansible_host=52.1.0.15
|
||||
infisical2 ansible_host=52.1.0.16
|
||||
infisical3 ansible_host=52.1.0.17
|
||||
```
|
||||
|
||||
## Full deployment example
|
||||
To make it easier to get started, we've provided a full deployment example that you can use to deploy Infisical in a high availability environment.
|
||||
|
||||
- This deployment does not use an external load balancer.
|
||||
- You **must** change the environment variables defined in the `playbook.yml` example.
|
||||
- You have update the IP addresses in the `inventory.ini` file to match your own network configuration.
|
||||
- You need to set the SSH key and ssh user in the `inventory.ini` file.
|
||||
|
||||
<Steps>
|
||||
<Step title="Install Ansible">
|
||||
Install Ansible using the pipx Python package manager.
|
||||
```bash
|
||||
pipx install --include-deps ansible
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step title="Install the Infisical deployment Ansible Role">
|
||||
Install the Infisical deployment role from Ansible Galaxy.
|
||||
```bash
|
||||
ansible-galaxy collection install infisical.infisical_core_ha_deployment
|
||||
```
|
||||
</Step>
|
||||
<Step title="Setup your hosts">
|
||||
|
||||
Create an `inventory.ini` file, and define your hosts and their IP addresses. You can use the example below as a template, and update the IP addresses to match your own network configuration.
|
||||
Make sure to set the SSH key and ssh user in the `inventory.ini` file. Please see the example below.
|
||||
|
||||
```ini example.inventory.ini
|
||||
[etcd]
|
||||
etcd1 ansible_host=52.1.0.3
|
||||
etcd2 ansible_host=52.1.0.4
|
||||
etcd3 ansible_host=52.1.0.5
|
||||
|
||||
[postgres]
|
||||
postgres1 ansible_host=52.1.0.6
|
||||
postgres2 ansible_host=52.1.0.7
|
||||
postgres3 ansible_host=52.1.0.8
|
||||
|
||||
[infisical]
|
||||
infisical1 ansible_host=52.1.0.15
|
||||
infisical2 ansible_host=52.1.0.16
|
||||
infisical3 ansible_host=52.1.0.17
|
||||
|
||||
[redis]
|
||||
redis1 ansible_host=52.1.0.9
|
||||
redis2 ansible_host=52.1.0.10
|
||||
redis3 ansible_host=52.1.0.11
|
||||
|
||||
[sentinel]
|
||||
sentinel1 ansible_host=52.1.0.12
|
||||
sentinel2 ansible_host=52.1.0.13
|
||||
sentinel3 ansible_host=52.1.0.14
|
||||
|
||||
[haproxy]
|
||||
internal_lb ansible_host=52.1.0.2
|
||||
|
||||
; This can be defined individually for each host, or globally for all hosts.
|
||||
; In this case the credentials are the same for all hosts, so we define them globally as seen below ([all:vars]).
|
||||
[all:vars]
|
||||
ansible_user=ubuntu
|
||||
ansible_ssh_private_key_file=./your-ssh-key.pem
|
||||
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
|
||||
```
|
||||
</Step>
|
||||
<Step title="Setup your Ansible playbook">
|
||||
The Ansible playbook is where you define which roles/tasks to execute on which hosts.
|
||||
|
||||
```yaml example.playbook.yml
|
||||
---
|
||||
# Important, we must gather facts from all hosts prior to running the roles to ensure we have all the information we need.
|
||||
- hosts: all
|
||||
gather_facts: true
|
||||
|
||||
- name: Set up etcd cluster
|
||||
hosts: etcd
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: etcd
|
||||
|
||||
- name: Set up PostgreSQL with Patroni
|
||||
hosts: postgres
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: postgres
|
||||
vars:
|
||||
postgres_super_user_password: "<ENTER_SUPERUSER_PASSWORD>" # Password for the 'postgres' database user
|
||||
|
||||
# A database with these credentials will be created on the leader node, and replicated to the secondary nodes.
|
||||
postgres_db_name: <ENTER_DB_NAME>
|
||||
postgres_user: <ENTER_DB_USER>
|
||||
postgres_user_password: <ENTER_DB_USER_PASSWORD>
|
||||
|
||||
etcd_hosts: "{{ groups['etcd'] }}"
|
||||
|
||||
- name: Setup Redis and Sentinel
|
||||
hosts: redis:sentinel
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: redis
|
||||
vars:
|
||||
redis_password: "<ENTER_REDIS_PASSWORD>"
|
||||
|
||||
- name: Set up HAProxy
|
||||
hosts: haproxy
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: haproxy
|
||||
vars:
|
||||
stats_user: "<ENTER_HAPROXY_STATS_USERNAME>"
|
||||
stats_password: "<ENTER_HAPROXY_STATS_PASSWORD>"
|
||||
|
||||
postgres_servers: "{{ groups['postgres'] }}"
|
||||
infisical_servers: "{{ groups['infisical'] }}"
|
||||
redis_servers: "{{ groups['redis'] }}"
|
||||
- name: Setup Infisical
|
||||
hosts: infisical
|
||||
become: true
|
||||
collections:
|
||||
- infisical.infisical_core_ha_deployment
|
||||
roles:
|
||||
- role: infisical
|
||||
env_vars:
|
||||
ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16
|
||||
AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32
|
||||
```
|
||||
</Step>
|
||||
<Step title="Run the Ansible playbook">
|
||||
After creating the `playbook.yml` and `inventory.ini` files, you can run the playbook using the following command
|
||||
```bash
|
||||
ansible-playbook -i inventory.ini playbook.yml
|
||||
```
|
||||
|
||||
This step may take upwards of 10 minutes to complete, depending on the number of nodes and the network speed.
|
||||
Once the playbook has completed, you should have a fully deployed high availability Infisical environment.
|
||||
|
||||
To access Infisical, you can try navigating to `http://52.1.0.2`, in order to view your newly deployed Infisical instance.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
## Post-deployment steps
|
||||
After deploying Infisical in a high availability environment, you should perform the following post-deployment steps:
|
||||
- Check your deployment to ensure that all services are running as expected. You can use the HAProxy monitoring panel to check the status of your services (http://52.1.0.2:7000/haproxy?stats)
|
||||
- Attempt to access the Infisical Core instances to ensure that they are accessible from the internal load balancer. (http://52.1.0.2)
|
||||
|
||||
A HAProxy stats page indicating success will look like the image below
|
||||

|
||||
|
||||
|
||||
## Security Considerations
|
||||
### Network Security
|
||||
Secure the network that your instances run on. While this falls outside the scope of Infisical deployment, it's crucial for overall security.
|
||||
AWS-specific recommendations:
|
||||
|
||||
Use Virtual Private Cloud (VPC) to isolate your infrastructure.
|
||||
Configure security groups to restrict inbound and outbound traffic.
|
||||
Use Network Access Control Lists (NACLs) for additional network-level security.
|
||||
|
||||
<Note>
|
||||
Please take note that the Infisical team cannot provide infrastructure support for **free self-hosted** deployments.<br/>If you need help with infrastructure, we recommend upgrading to a [paid plan](https://infisical.com/pricing) which includes infrastructure support.
|
||||
|
||||
You can also join our community [Slack](https://infisical.com/slack) for help and support from the community.
|
||||
</Note>
|
||||
|
||||
|
||||
### Troubleshooting
|
||||
<Accordion title="Ansible: Failed to set permissions on the temporary files Ansible needs to create when becoming an unprivileged user">
|
||||
If you encounter this issue, please update your ansible config (`ansible.cfg`) file with the following configuration:
|
||||
```ini
|
||||
[defaults]
|
||||
allow_world_readable_tmpfiles = true
|
||||
```
|
||||
|
||||
You can read more about the solution [here](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/sh_shell.html#parameter-world_readable_temp)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="I'm unable to connect to access the Infisical instance on the web">
|
||||
This issue can be caused by a number of reasons, mostly realted to the network configuration. Here are a few things you can check:
|
||||
1. Ensure that the firewall is not blocking the connection. You can check this by running `ufw status`. Ensure that port 80 is open.
|
||||
2. If you're using a cloud provider like AWS or GCP, ensure that the security group allows traffic on port 80.
|
||||
3. Ensure that the HAProxy service is running. You can check this by running `systemctl status haproxy`.
|
||||
4. Ensure that the Infisical service is running. You can check this by running `systemctl status infisical`.
|
||||
</Accordion>
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "AWS ECS"
|
||||
title: "AWS ECS (HA)"
|
||||
description: "Reference architecture for self-hosting Infisical on AWS ECS"
|
||||
---
|
||||
|
||||
|
@ -0,0 +1,383 @@
|
||||
---
|
||||
title: "Linux (HA)"
|
||||
description: "Infisical High Availability Deployment architecture for Linux"
|
||||
---
|
||||
|
||||
This guide describes how to achieve a highly available deployment of Infisical on Linux machines without containerization. The architecture provided serves as a foundation for minimum high availability, which you can scale based on your specific requirements.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||

|
||||
|
||||
The deployment consists of the following key components:
|
||||
|
||||
| Service | Nodes | Recommended Specs | GCP Instance | AWS Instance |
|
||||
|---------------------------|-------|---------------------------|-----------------|--------------|
|
||||
| External Load Balancer | 1 | 4 vCPU, 4 GB memory | n1-highcpu-4 | c5n.xlarge |
|
||||
| Internal Load Balancer | 1 | 4 vCPU, 4 GB memory | n1-highcpu-4 | c5n.xlarge |
|
||||
| Etcd Cluster | 3 | 4 vCPU, 4 GB memory | n1-highcpu-4 | c5n.xlarge |
|
||||
| PostgreSQL Cluster | 3 | 2 vCPU, 8 GB memory | n1-standard-2 | m5.large |
|
||||
| Redis + Sentinel | 3+3 | 2 vCPU, 8 GB memory | n1-standard-2 | m5.large |
|
||||
| Infisical Core | 3 | 2 vCPU, 4 GB memory | n1-highcpu-2 | c5.large |
|
||||
|
||||
### Network Architecture
|
||||
|
||||
All servers operate within the 52.1.0.0/24 private network range with the following IP assignments:
|
||||
|
||||
| Service | IP Address |
|
||||
|----------------------|------------|
|
||||
| External Load Balancer| 52.1.0.1 |
|
||||
| Internal Load Balancer| 52.1.0.2 |
|
||||
| Etcd Node 1 | 52.1.0.3 |
|
||||
| Etcd Node 2 | 52.1.0.4 |
|
||||
| Etcd Node 3 | 52.1.0.5 |
|
||||
| PostgreSQL Node 1 | 52.1.0.6 |
|
||||
| PostgreSQL Node 2 | 52.1.0.7 |
|
||||
| PostgreSQL Node 3 | 52.1.0.8 |
|
||||
| Redis Node 1 | 52.1.0.9 |
|
||||
| Redis Node 2 | 52.1.0.10 |
|
||||
| Redis Node 3 | 52.1.0.11 |
|
||||
| Sentinel Node 1 | 52.1.0.12 |
|
||||
| Sentinel Node 2 | 52.1.0.13 |
|
||||
| Sentinel Node 3 | 52.1.0.14 |
|
||||
| Infisical Core 1 | 52.1.0.15 |
|
||||
| Infisical Core 2 | 52.1.0.16 |
|
||||
| Infisical Core 3 | 52.1.0.17 |
|
||||
|
||||
## Component Setup Guide
|
||||
|
||||
### 1. Configure Etcd Cluster
|
||||
|
||||
The Etcd cluster is needed for leader election in the PostgreSQL HA setup. Skip this step if using managed PostgreSQL.
|
||||
|
||||
1. Install Etcd on each node:
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install etcd
|
||||
```
|
||||
|
||||
2. Configure each node with unique identifiers and cluster membership. Example configuration for Node 1 (`/etc/etcd/etcd.conf`):
|
||||
```yaml
|
||||
name: etcd1
|
||||
data-dir: /var/lib/etcd
|
||||
initial-cluster-state: new
|
||||
initial-cluster-token: etcd-cluster-1
|
||||
initial-cluster: etcd1=http://52.1.0.3:2380,etcd2=http://52.1.0.4:2380,etcd3=http://52.1.0.5:2380
|
||||
initial-advertise-peer-urls: http://52.1.0.3:2380
|
||||
listen-peer-urls: http://52.1.0.3:2380
|
||||
listen-client-urls: http://52.1.0.3:2379,http://127.0.0.1:2379
|
||||
advertise-client-urls: http://52.1.0.3:2379
|
||||
```
|
||||
|
||||
### 2. Configure PostgreSQL
|
||||
|
||||
For production deployments, you have two options for highly available PostgreSQL:
|
||||
|
||||
#### Option A: Managed PostgreSQL Service (Recommended for Most Users)
|
||||
|
||||
Use cloud provider managed services:
|
||||
- AWS: Amazon RDS for PostgreSQL with Multi-AZ
|
||||
- GCP: Cloud SQL for PostgreSQL with HA configuration
|
||||
- Azure: Azure Database for PostgreSQL with zone redundant HA
|
||||
|
||||
These services handle replication, failover, and maintenance automatically.
|
||||
|
||||
#### Option B: Self-Managed PostgreSQL Cluster
|
||||
|
||||
Full HA installation guide of PostgreSQL is beyond the scope of this document. However, we have provided an overview of resources and code snippets below to guide your deployment.
|
||||
|
||||
1. Required Components:
|
||||
- PostgreSQL 14+ on each node
|
||||
- Patroni for cluster management
|
||||
- Etcd for distributed consensus
|
||||
|
||||
2. Documentation we recommend you read:
|
||||
- [Complete Patroni Setup Guide](https://patroni.readthedocs.io/en/latest/README.html)
|
||||
- [PostgreSQL Replication Documentation](https://www.postgresql.org/docs/current/high-availability.html)
|
||||
|
||||
3. Key Steps Overview:
|
||||
```bash
|
||||
# 1. Install requirements on each PostgreSQL node
|
||||
sudo apt update
|
||||
sudo apt install -y postgresql-14 postgresql-contrib-14 python3-pip
|
||||
pip3 install patroni[etcd] psycopg2-binary
|
||||
|
||||
# 2. Create Patroni config directory
|
||||
sudo mkdir /etc/patroni
|
||||
sudo chown postgres:postgres /etc/patroni
|
||||
|
||||
# 3. Create Patroni configuration (example for first node)
|
||||
# /etc/patroni/config.yml - REQUIRES CAREFUL CUSTOMIZATION
|
||||
```
|
||||
|
||||
```yaml
|
||||
scope: infisical-cluster
|
||||
namespace: /db/
|
||||
name: postgresql1
|
||||
|
||||
restapi:
|
||||
listen: 52.1.0.6:8008
|
||||
connect_address: 52.1.0.6:8008
|
||||
|
||||
etcd:
|
||||
hosts: 52.1.0.3:2379,52.1.0.4:2379,52.1.0.5:2379
|
||||
|
||||
bootstrap:
|
||||
dcs:
|
||||
ttl: 30
|
||||
loop_wait: 10
|
||||
retry_timeout: 10
|
||||
maximum_lag_on_failover: 1048576
|
||||
postgresql:
|
||||
use_pg_rewind: true
|
||||
parameters:
|
||||
max_connections: 1000
|
||||
shared_buffers: 2GB
|
||||
work_mem: 8MB
|
||||
max_worker_processes: 8
|
||||
max_parallel_workers_per_gather: 4
|
||||
max_parallel_workers: 8
|
||||
wal_level: replica
|
||||
hot_standby: "on"
|
||||
max_wal_senders: 10
|
||||
max_replication_slots: 10
|
||||
hot_standby_feedback: "on"
|
||||
```
|
||||
|
||||
4. Important considerations:
|
||||
- Proper disk configuration for WAL and data directories
|
||||
- Network latency between nodes
|
||||
- Backup strategy and point-in-time recovery
|
||||
- Monitoring and alerting setup
|
||||
- Connection pooling configuration
|
||||
- Security and network access controls
|
||||
|
||||
5. Recommended readings:
|
||||
- [PostgreSQL Backup and Recovery](https://www.postgresql.org/docs/current/backup.html)
|
||||
- [PostgreSQL Monitoring](https://www.postgresql.org/docs/current/monitoring.html)
|
||||
|
||||
### 3. Configure Redis and Sentinel
|
||||
|
||||
Similar to PostgreSQL, a full HA Redis setup guide is beyond the scope of this document. Below are the key resources and considerations for your deployment.
|
||||
|
||||
#### Option A: Managed Redis Service (Recommended for Most Users)
|
||||
|
||||
Use cloud provider managed Redis services:
|
||||
- AWS: ElastiCache for Redis with Multi-AZ
|
||||
- GCP: Memorystore for Redis with HA
|
||||
- Azure: Azure Cache for Redis with zone redundancy
|
||||
|
||||
Follow your cloud provider's documentation:
|
||||
- [AWS ElastiCache Documentation](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/WhatIs.html)
|
||||
- [GCP Memorystore Documentation](https://cloud.google.com/memorystore/docs/redis)
|
||||
- [Azure Redis Cache Documentation](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/)
|
||||
|
||||
#### Option B: Self-Managed Redis Cluster
|
||||
|
||||
Setting up a production Redis HA cluster requires understanding several components. Refer to these linked resources:
|
||||
|
||||
1. Required Reading:
|
||||
- [Redis Sentinel Documentation](https://redis.io/docs/management/sentinel/)
|
||||
- [Redis Replication Guide](https://redis.io/topics/replication)
|
||||
- [Redis Security Guide](https://redis.io/topics/security)
|
||||
|
||||
2. Key Steps Overview:
|
||||
```bash
|
||||
# 1. Install Redis on all nodes
|
||||
sudo apt update
|
||||
sudo apt install redis-server
|
||||
|
||||
# 2. Configure master node (52.1.0.9)
|
||||
# /etc/redis/redis.conf
|
||||
```
|
||||
|
||||
```conf
|
||||
bind 52.1.0.9
|
||||
port 6379
|
||||
dir /var/lib/redis
|
||||
maxmemory 3gb
|
||||
maxmemory-policy noeviction
|
||||
requirepass "your_redis_password"
|
||||
masterauth "your_redis_password"
|
||||
```
|
||||
|
||||
3. Configure replica nodes (`52.1.0.10`, `52.1.0.11`):
|
||||
```conf
|
||||
bind 52.1.0.10 # Change for each replica
|
||||
port 6379
|
||||
dir /var/lib/redis
|
||||
replicaof 52.1.0.9 6379
|
||||
masterauth "your_redis_password"
|
||||
requirepass "your_redis_password"
|
||||
```
|
||||
|
||||
4. Configure Sentinel nodes (`52.1.0.12`, `52.1.0.13`, `52.1.0.14`):
|
||||
```conf
|
||||
port 26379
|
||||
sentinel monitor mymaster 52.1.0.9 6379 2
|
||||
sentinel auth-pass mymaster "your_redis_password"
|
||||
sentinel down-after-milliseconds mymaster 5000
|
||||
sentinel failover-timeout mymaster 60000
|
||||
sentinel parallel-syncs mymaster 1
|
||||
```
|
||||
|
||||
5. Recommended Additional Reading:
|
||||
- [Redis High Availability Tools](https://redis.io/topics/high-availability)
|
||||
- [Redis Sentinel Client Implementation](https://redis.io/topics/sentinel-clients)
|
||||
|
||||
### 4. Configure HAProxy Load Balancer
|
||||
|
||||
Install and configure HAProxy for internal load balancing:
|
||||
|
||||
```conf ha-proxy-config
|
||||
global
|
||||
maxconn 10000
|
||||
log stdout format raw local0
|
||||
|
||||
defaults
|
||||
log global
|
||||
mode tcp
|
||||
retries 3
|
||||
timeout client 30m
|
||||
timeout connect 10s
|
||||
timeout server 30m
|
||||
timeout check 5s
|
||||
|
||||
listen stats
|
||||
mode http
|
||||
bind *:7000
|
||||
stats enable
|
||||
stats uri /
|
||||
|
||||
resolvers hostdns
|
||||
nameserver dns 127.0.0.11:53
|
||||
resolve_retries 3
|
||||
timeout resolve 1s
|
||||
timeout retry 1s
|
||||
hold valid 5s
|
||||
|
||||
frontend postgres_master
|
||||
bind *:5000
|
||||
default_backend postgres_master_backend
|
||||
|
||||
frontend postgres_replicas
|
||||
bind *:5001
|
||||
default_backend postgres_replica_backend
|
||||
|
||||
backend postgres_master_backend
|
||||
option httpchk GET /master
|
||||
http-check expect status 200
|
||||
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
|
||||
server postgres-1 52.1.0.6:5432 check port 8008
|
||||
server postgres-2 52.1.0.7:5432 check port 8008
|
||||
server postgres-3 52.1.0.8:5432 check port 8008
|
||||
|
||||
backend postgres_replica_backend
|
||||
option httpchk GET /replica
|
||||
http-check expect status 200
|
||||
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
|
||||
server postgres-1 52.1.0.6:5432 check port 8008
|
||||
server postgres-2 52.1.0.7:5432 check port 8008
|
||||
server postgres-3 52.1.0.8:5432 check port 8008
|
||||
|
||||
frontend redis_master_frontend
|
||||
bind *:6379
|
||||
default_backend redis_master_backend
|
||||
|
||||
backend redis_master_backend
|
||||
option tcp-check
|
||||
tcp-check send AUTH\ 123456\r\n
|
||||
tcp-check expect string +OK
|
||||
tcp-check send PING\r\n
|
||||
tcp-check expect string +PONG
|
||||
tcp-check send info\ replication\r\n
|
||||
tcp-check expect string role:master
|
||||
tcp-check send QUIT\r\n
|
||||
tcp-check expect string +OK
|
||||
server redis-1 52.1.0.9:6379 check inter 1s
|
||||
server redis-2 52.1.0.10:6379 check inter 1s
|
||||
server redis-3 52.1.0.11:6379 check inter 1s
|
||||
|
||||
frontend infisical_frontend
|
||||
bind *:80
|
||||
default_backend infisical_backend
|
||||
|
||||
backend infisical_backend
|
||||
option httpchk GET /api/status
|
||||
http-check expect status 200
|
||||
server infisical-1 52.1.0.15:8080 check inter 1s
|
||||
server infisical-2 52.1.0.16:8080 check inter 1s
|
||||
server infisical-3 52.1.0.17:8080 check inter 1s
|
||||
```
|
||||
|
||||
### 5. Deploy Infisical Core
|
||||
<Tabs>
|
||||
<Tab title="Debian/Ubuntu">
|
||||
First, add the Infisical repository:
|
||||
```bash
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-core/setup.deb.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
Then install Infisical:
|
||||
```bash
|
||||
sudo apt-get update && sudo apt-get install -y infisical-core
|
||||
```
|
||||
|
||||
<Info>
|
||||
For production environments, we strongly recommend installing a specific version of the package to maintain consistency across reinstalls. View available versions at [Infisical Package Versions](https://cloudsmith.io/~infisical/repos/infisical-core/packages/).
|
||||
</Info>
|
||||
</Tab>
|
||||
|
||||
<Tab title="RedHat/CentOS/Amazon Linux">
|
||||
First, add the Infisical repository:
|
||||
```bash
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-core/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
Then install Infisical:
|
||||
```bash
|
||||
sudo yum install infisical-core
|
||||
```
|
||||
|
||||
<Info>
|
||||
For production environments, we strongly recommend installing a specific version of the package to maintain consistency across reinstalls. View available versions at [Infisical Package Versions](https://cloudsmith.io/~infisical/repos/infisical-core/packages/).
|
||||
</Info>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Next, create configuration file `/etc/infisical/infisical.rb` with the following:
|
||||
|
||||
```ruby
|
||||
infisical_core['ENCRYPTION_KEY'] = 'your-secure-encryption-key'
|
||||
infisical_core['AUTH_SECRET'] = 'your-secure-auth-secret'
|
||||
|
||||
infisical_core['DB_CONNECTION_URI'] = 'postgres://user:pass@52.1.0.2:5000/infisical'
|
||||
infisical_core['REDIS_URL'] = 'redis://52.1.0.2:6379'
|
||||
|
||||
infisical_core['PORT'] = 8080
|
||||
```
|
||||
|
||||
To generate `ENCRYPTION_KEY` and `AUTH_SECRET` view the [following configurations documentation here](/self-hosting/configuration/envars).
|
||||
|
||||
If you are using managed services for either Postgres or Redis, please replace the values of the secrets accordingly.
|
||||
|
||||
|
||||
Lastly, start and verify each node running infisical-core:
|
||||
```bash
|
||||
sudo infisical-ctl reconfigure
|
||||
sudo infisical-ctl status
|
||||
```
|
||||
|
||||
## Monitoring and Maintenance
|
||||
|
||||
1. Monitor HAProxy stats: `http://52.1.0.2:7000/haproxy?stats`
|
||||
2. Monitor Infisical logs: `sudo infisical-ctl tail`
|
||||
3. Check cluster health:
|
||||
- Etcd: `etcdctl cluster-health`
|
||||
- PostgreSQL: `patronictl list`
|
||||
- Redis: `redis-cli info replication`
|
@ -11,7 +11,7 @@ import { SecretType } from "@app/hooks/api/types";
|
||||
import Button from "../basic/buttons/Button";
|
||||
import Error from "../basic/Error";
|
||||
import { createNotification } from "../notifications";
|
||||
import { parseDotEnv } from "../utilities/parseDotEnv";
|
||||
import { parseDotEnv } from "../utilities/parseSecrets";
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
|
||||
interface DropZoneProps {
|
||||
|
@ -6,7 +6,7 @@ const LINE =
|
||||
* @param {ArrayBuffer} src - source buffer
|
||||
* @returns {String} text - text of buffer
|
||||
*/
|
||||
export function parseDotEnv(src: ArrayBuffer) {
|
||||
export function parseDotEnv(src: ArrayBuffer | string) {
|
||||
const object: {
|
||||
[key: string]: { value: string; comments: string[] };
|
||||
} = {};
|
||||
@ -65,3 +65,15 @@ export function parseDotEnv(src: ArrayBuffer) {
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
export const parseJson = (src: ArrayBuffer | string) => {
|
||||
const file = src.toString();
|
||||
const formatedData: Record<string, string> = JSON.parse(file);
|
||||
const env: Record<string, { value: string; comments: string[] }> = {};
|
||||
Object.keys(formatedData).forEach((key) => {
|
||||
if (typeof formatedData[key] === "string") {
|
||||
env[key] = { value: formatedData[key], comments: [] };
|
||||
}
|
||||
});
|
||||
return env;
|
||||
};
|
@ -83,6 +83,7 @@ export type FormControlProps = {
|
||||
className?: string;
|
||||
icon?: ReactNode;
|
||||
tooltipText?: ReactElement | string;
|
||||
tooltipClassName?: string;
|
||||
};
|
||||
|
||||
export const FormControl = ({
|
||||
@ -96,7 +97,8 @@ export const FormControl = ({
|
||||
isError,
|
||||
icon,
|
||||
className,
|
||||
tooltipText
|
||||
tooltipText,
|
||||
tooltipClassName
|
||||
}: FormControlProps): JSX.Element => {
|
||||
return (
|
||||
<div className={twMerge("mb-4", className)}>
|
||||
@ -108,6 +110,7 @@ export const FormControl = ({
|
||||
id={id}
|
||||
icon={icon}
|
||||
tooltipText={tooltipText}
|
||||
tooltipClassName={tooltipClassName}
|
||||
/>
|
||||
) : (
|
||||
label
|
||||
|
@ -5,6 +5,7 @@ import axios from "axios";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import {
|
||||
DashboardProjectSecretsByKeys,
|
||||
DashboardProjectSecretsDetails,
|
||||
DashboardProjectSecretsDetailsResponse,
|
||||
DashboardProjectSecretsOverview,
|
||||
@ -12,6 +13,7 @@ import {
|
||||
DashboardSecretsOrderBy,
|
||||
TDashboardProjectSecretsQuickSearch,
|
||||
TDashboardProjectSecretsQuickSearchResponse,
|
||||
TGetDashboardProjectSecretsByKeys,
|
||||
TGetDashboardProjectSecretsDetailsDTO,
|
||||
TGetDashboardProjectSecretsOverviewDTO,
|
||||
TGetDashboardProjectSecretsQuickSearchDTO
|
||||
@ -101,6 +103,23 @@ export const fetchProjectSecretsDetails = async ({
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchDashboardProjectSecretsByKeys = async ({
|
||||
keys,
|
||||
...params
|
||||
}: TGetDashboardProjectSecretsByKeys) => {
|
||||
const { data } = await apiRequest.get<DashboardProjectSecretsByKeys>(
|
||||
"/api/v1/dashboard/secrets-by-keys",
|
||||
{
|
||||
params: {
|
||||
...params,
|
||||
keys: encodeURIComponent(keys.join(","))
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetProjectSecretsOverview = (
|
||||
{
|
||||
projectId,
|
||||
|
@ -29,6 +29,10 @@ export type DashboardProjectSecretsDetailsResponse = {
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
export type DashboardProjectSecretsByKeys = {
|
||||
secrets: SecretV3Raw[];
|
||||
};
|
||||
|
||||
export type DashboardProjectSecretsOverview = Omit<
|
||||
DashboardProjectSecretsOverviewResponse,
|
||||
"secrets"
|
||||
@ -89,3 +93,10 @@ export type TGetDashboardProjectSecretsQuickSearchDTO = {
|
||||
search: string;
|
||||
environments: string[];
|
||||
};
|
||||
|
||||
export type TGetDashboardProjectSecretsByKeys = {
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
keys: string[];
|
||||
};
|
||||
|
@ -552,7 +552,6 @@ const SecretMainPageContent = () => {
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<SecretDropzone
|
||||
secrets={secrets}
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
|
@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faClone,
|
||||
faFileImport,
|
||||
faKey,
|
||||
faSearch,
|
||||
faSquareCheck,
|
||||
@ -151,6 +152,7 @@ export const CopySecretsFromBoard = ({
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
||||
onClick={() => onToggle(true)}
|
||||
isDisabled={!isAllowed}
|
||||
variant="star"
|
||||
|
@ -0,0 +1,165 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faInfoCircle, faPaste } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { parseDotEnv, parseJson } from "@app/components/utilities/parseSecrets";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalTrigger,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
isSmaller?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
value: z.string().trim()
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
const PasteEnvForm = ({ onParsedEnv }: Pick<Props, "onParsedEnv">) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { isDirty, errors },
|
||||
setError,
|
||||
setFocus
|
||||
} = useForm<TForm>({ defaultValues: { value: "" }, resolver: zodResolver(formSchema) });
|
||||
|
||||
const onSubmit = ({ value }: TForm) => {
|
||||
let env: Record<string, { value: string; comments: string[] }>;
|
||||
try {
|
||||
env = parseJson(value);
|
||||
} catch (e) {
|
||||
// not json, parse as env
|
||||
env = parseDotEnv(value);
|
||||
}
|
||||
|
||||
if (!Object.keys(env).length) {
|
||||
setError("value", {
|
||||
message: "No secrets found. Please make sure the provided format is valid."
|
||||
});
|
||||
setFocus("value");
|
||||
return;
|
||||
}
|
||||
|
||||
onParsedEnv(env);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormControl
|
||||
label="Secret Values"
|
||||
isError={Boolean(errors.value)}
|
||||
errorText={errors.value?.message}
|
||||
icon={<FontAwesomeIcon size="sm" className="text-mineshaft-400" icon={faInfoCircle} />}
|
||||
tooltipClassName="max-w-lg px-2 whitespace-pre-line"
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>Example Formats:</p>
|
||||
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
|
||||
{/* eslint-disable-next-line react/jsx-no-comment-textnodes */}
|
||||
<p className="text-mineshaft-400">// .json</p>
|
||||
{JSON.stringify(
|
||||
{
|
||||
APP_NAME: "example-service",
|
||||
APP_VERSION: "1.2.3",
|
||||
NODE_ENV: "production"
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
|
||||
<p className="text-mineshaft-400"># .env</p>
|
||||
<p>APP_NAME="example-service"</p>
|
||||
<p>APP_VERSION="1.2.3"</p>
|
||||
<p>NODE_ENV="production"</p>
|
||||
</pre>
|
||||
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
|
||||
<p className="text-mineshaft-400"># .yml</p>
|
||||
<p>APP_NAME: example-service</p>
|
||||
<p>APP_VERSION: 1.2.3</p>
|
||||
<p>NODE_ENV: production</p>
|
||||
</pre>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TextArea
|
||||
{...register("value")}
|
||||
placeholder="Paste secrets in .json, .yml or .env format..."
|
||||
className="h-[60vh] !resize-none"
|
||||
/>
|
||||
</FormControl>
|
||||
<Button isDisabled={!isDirty} type="submit">
|
||||
Import Secrets
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const PasteSecretEnvModal = ({
|
||||
isSmaller,
|
||||
isOpen,
|
||||
onParsedEnv,
|
||||
onToggle,
|
||||
environment,
|
||||
secretPath
|
||||
}: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||
<ModalTrigger asChild>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPaste} />}
|
||||
onClick={() => onToggle(true)}
|
||||
isDisabled={!isAllowed}
|
||||
variant="star"
|
||||
size={isSmaller ? "xs" : "sm"}
|
||||
>
|
||||
Paste Secrets
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Past Secret Values"
|
||||
subTitle="Paste values in .env, .json or .yml format"
|
||||
>
|
||||
<PasteEnvForm
|
||||
onParsedEnv={(value) => {
|
||||
onToggle(false);
|
||||
onParsedEnv(value);
|
||||
}}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { ChangeEvent, DragEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@ -9,30 +9,22 @@ import { twMerge } from "tailwind-merge";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
|
||||
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
|
||||
import { parseDotEnv, parseJson } from "@app/components/utilities/parseSecrets";
|
||||
import { Button, Modal, ModalContent } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api";
|
||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import {
|
||||
dashboardKeys,
|
||||
fetchDashboardProjectSecretsByKeys
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
|
||||
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
||||
import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
|
||||
|
||||
const parseJson = (src: ArrayBuffer) => {
|
||||
const file = src.toString();
|
||||
const formatedData: Record<string, string> = JSON.parse(file);
|
||||
const env: Record<string, { value: string; comments: string[] }> = {};
|
||||
Object.keys(formatedData).forEach((key) => {
|
||||
if (typeof formatedData[key] === "string") {
|
||||
env[key] = { value: formatedData[key], comments: [] };
|
||||
}
|
||||
});
|
||||
return env;
|
||||
};
|
||||
import { PasteSecretEnvModal } from "./PasteSecretEnvModal";
|
||||
|
||||
type TParsedEnv = Record<string, { value: string; comments: string[] }>;
|
||||
type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv };
|
||||
@ -43,7 +35,6 @@ type Props = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets?: SecretV3RawSanitized[];
|
||||
isProtectedBranch?: boolean;
|
||||
};
|
||||
|
||||
@ -53,7 +44,6 @@ export const SecretDropzone = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secrets = [],
|
||||
isProtectedBranch = false
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
@ -62,7 +52,8 @@ export const SecretDropzone = ({
|
||||
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"importSecEnv",
|
||||
"overlapKeyWarning"
|
||||
"confirmUpload",
|
||||
"pasteSecEnv"
|
||||
] as const);
|
||||
const queryClient = useQueryClient();
|
||||
const { openPopUp } = usePopUpAction();
|
||||
@ -86,20 +77,10 @@ export const SecretDropzone = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleParsedEnv = (env: TParsedEnv) => {
|
||||
const secretsGroupedByKey = secrets?.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr.key]: true }),
|
||||
{}
|
||||
);
|
||||
const overlappedSecrets = Object.keys(env)
|
||||
.filter((secKey) => secretsGroupedByKey?.[secKey])
|
||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||
const handleParsedEnv = async (env: TParsedEnv) => {
|
||||
const envSecretKeys = Object.keys(env);
|
||||
|
||||
const nonOverlappedSecrets = Object.keys(env)
|
||||
.filter((secKey) => !secretsGroupedByKey?.[secKey])
|
||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||
|
||||
if (!Object.keys(overlappedSecrets).length && !Object.keys(nonOverlappedSecrets).length) {
|
||||
if (!envSecretKeys.length) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to find secrets"
|
||||
@ -107,10 +88,42 @@ export const SecretDropzone = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("overlapKeyWarning", {
|
||||
update: overlappedSecrets,
|
||||
create: nonOverlappedSecrets
|
||||
});
|
||||
try {
|
||||
setIsLoading.on();
|
||||
const { secrets: existingSecrets } = await fetchDashboardProjectSecretsByKeys({
|
||||
secretPath,
|
||||
environment,
|
||||
projectId: workspaceId,
|
||||
keys: envSecretKeys
|
||||
});
|
||||
|
||||
const secretsGroupedByKey = existingSecrets.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr.secretKey]: true }),
|
||||
{}
|
||||
);
|
||||
|
||||
const updateSecrets = Object.keys(env)
|
||||
.filter((secKey) => secretsGroupedByKey[secKey])
|
||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||
|
||||
const createSecrets = Object.keys(env)
|
||||
.filter((secKey) => !secretsGroupedByKey[secKey])
|
||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||
|
||||
handlePopUpOpen("confirmUpload", {
|
||||
update: updateSecrets,
|
||||
create: createSecrets
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
createNotification({
|
||||
text: "Failed to check for secret conflicts",
|
||||
type: "error"
|
||||
});
|
||||
handlePopUpClose("confirmUpload");
|
||||
} finally {
|
||||
setIsLoading.off();
|
||||
}
|
||||
};
|
||||
|
||||
const parseFile = (file?: File, isJson?: boolean) => {
|
||||
@ -160,7 +173,7 @@ export const SecretDropzone = ({
|
||||
};
|
||||
|
||||
const handleSaveSecrets = async () => {
|
||||
const { update, create } = popUp?.overlapKeyWarning?.data as TSecOverwriteOpt;
|
||||
const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt;
|
||||
try {
|
||||
if (Object.keys(create || {}).length) {
|
||||
await createSecretBatch({
|
||||
@ -195,7 +208,7 @@ export const SecretDropzone = ({
|
||||
dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
|
||||
);
|
||||
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
|
||||
handlePopUpClose("overlapKeyWarning");
|
||||
handlePopUpClose("confirmUpload");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: isProtectedBranch
|
||||
@ -211,10 +224,16 @@ export const SecretDropzone = ({
|
||||
}
|
||||
};
|
||||
|
||||
const isUploadedDuplicateSecretsEmpty = !Object.keys(
|
||||
(popUp.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {}
|
||||
const createSecretCount = Object.keys(
|
||||
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {}
|
||||
).length;
|
||||
|
||||
const updateSecretCount = Object.keys(
|
||||
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.update || {}
|
||||
).length;
|
||||
|
||||
const isNonConflictingUpload = !updateSecretCount;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@ -278,7 +297,15 @@ export const SecretDropzone = ({
|
||||
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-8">
|
||||
<div className="flex flex-col items-center justify-center gap-4 lg:flex-row">
|
||||
<PasteSecretEnvModal
|
||||
isOpen={popUp.pasteSecEnv.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("pasteSecEnv", isOpen)}
|
||||
onParsedEnv={handleParsedEnv}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
isSmaller={isSmaller}
|
||||
/>
|
||||
<CopySecretsFromBoard
|
||||
isOpen={popUp.importSecEnv.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("importSecEnv", isOpen)}
|
||||
@ -301,11 +328,12 @@ export const SecretDropzone = ({
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
|
||||
variant="star"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add a new secret
|
||||
Add a New Secret
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@ -315,25 +343,25 @@ export const SecretDropzone = ({
|
||||
)}
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={popUp?.overlapKeyWarning?.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle("overlapKeyWarning", open)}
|
||||
isOpen={popUp?.confirmUpload?.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle("confirmUpload", open)}
|
||||
>
|
||||
<ModalContent
|
||||
title={isUploadedDuplicateSecretsEmpty ? "Confirmation" : "Duplicate Secrets!!"}
|
||||
title="Confirm Secret Upload"
|
||||
footerContent={[
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
colorSchema={isUploadedDuplicateSecretsEmpty ? "primary" : "danger"}
|
||||
colorSchema={isNonConflictingUpload ? "primary" : "danger"}
|
||||
key="overwrite-btn"
|
||||
onClick={handleSaveSecrets}
|
||||
>
|
||||
{isUploadedDuplicateSecretsEmpty ? "Upload" : "Overwrite"}
|
||||
{isNonConflictingUpload ? "Upload" : "Overwrite"}
|
||||
</Button>,
|
||||
<Button
|
||||
key="keep-old-btn"
|
||||
className="mr-4"
|
||||
onClick={() => handlePopUpClose("overlapKeyWarning")}
|
||||
className="ml-4"
|
||||
onClick={() => handlePopUpClose("confirmUpload")}
|
||||
variant="outline_bg"
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
@ -341,17 +369,27 @@ export const SecretDropzone = ({
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
{isUploadedDuplicateSecretsEmpty ? (
|
||||
<div>Upload secrets from this file</div>
|
||||
{isNonConflictingUpload ? (
|
||||
<div>
|
||||
Are you sure you want to import {createSecretCount} secret
|
||||
{createSecretCount > 1 ? "s" : ""} to this environment?
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-2 text-gray-300">
|
||||
<div>Your file contains following duplicate secrets</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{Object.keys((popUp?.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {})
|
||||
<div className="flex flex-col text-gray-300">
|
||||
<div>Your project already contains the following {updateSecretCount} secrets:</div>
|
||||
<div className="mt-2 text-sm text-gray-400">
|
||||
{Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {})
|
||||
?.map((key) => key)
|
||||
.join(", ")}
|
||||
</div>
|
||||
<div>Are you sure you want to overwrite these secrets and create other ones?</div>
|
||||
<div className="mt-6">
|
||||
Are you sure you want to overwrite these secrets
|
||||
{createSecretCount > 0
|
||||
? ` and import ${createSecretCount} new
|
||||
one${createSecretCount > 1 ? "s" : ""}`
|
||||
: ""}
|
||||
?
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
|
9
npm/.eslintrc.json
Normal file
9
npm/.eslintrc.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest"
|
||||
}
|
||||
}
|
71
npm/README.md
Normal file
71
npm/README.md
Normal file
@ -0,0 +1,71 @@
|
||||
<h1 align="center">Infisical</h1>
|
||||
<p align="center">
|
||||
<p align="center"><b>The open-source secret management platform</b>: Sync secrets/configs across your team/infrastructure and prevent secret leaks.</p>
|
||||
</p>
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://infisical.com/slack">Slack</a> |
|
||||
<a href="https://infisical.com/">Infisical Cloud</a> |
|
||||
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
|
||||
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
|
||||
<a href="https://www.infisical.com">Website</a> |
|
||||
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
|
||||
</h4>
|
||||
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://github.com/Infisical/infisical/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Infisical is released under the MIT license." />
|
||||
</a>
|
||||
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
|
||||
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
|
||||
</a>
|
||||
<a href="https://github.com/Infisical/infisical/issues">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
||||
</a>
|
||||
<a href="https://cloudsmith.io/~infisical/repos/">
|
||||
<img src="https://img.shields.io/badge/Downloads-6.95M-orange" alt="Cloudsmith downloads" />
|
||||
</a>
|
||||
<a href="https://infisical.com/slack">
|
||||
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
||||
</a>
|
||||
<a href="https://twitter.com/infisical">
|
||||
<img src="https://img.shields.io/twitter/follow/infisical?label=Follow" alt="Infisical Twitter" />
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
### Introduction
|
||||
|
||||
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their application configuration and secrets like API keys and database credentials as well as manage their internal PKI.
|
||||
|
||||
We're on a mission to make security tooling more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
The Infisical CLI NPM package serves as a new installation method in addition to our [existing installation methods](https://infisical.com/docs/cli/overview).
|
||||
|
||||
After installing the CLI with the command below, you'll be able to use the infisical CLI across your machine.
|
||||
|
||||
```bash
|
||||
$ npm install -g @infisical/cli
|
||||
```
|
||||
|
||||
Full example:
|
||||
```bash
|
||||
# Install the Infisical CLI
|
||||
$ npm install -g @infisical/cli
|
||||
|
||||
# Authenticate with the Infisical CLI
|
||||
$ infisical login
|
||||
|
||||
# Initialize your Infisical CLI
|
||||
$ infisical init
|
||||
|
||||
# List your secrets with Infisical CLI
|
||||
$ infisical secrets
|
||||
```
|
||||
|
||||
|
||||
### Documentation
|
||||
Our full CLI documentation can be found [here](https://infisical.com/docs/cli/usage).
|
141
npm/package-lock.json
generated
Normal file
141
npm/package-lock.json
generated
Normal file
@ -0,0 +1,141 @@
|
||||
{
|
||||
"name": "@infisical/cli",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@infisical/cli",
|
||||
"version": "0.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"tar": "^6.2.0",
|
||||
"yauzl": "^3.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"infisical": "bin/infisical"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
|
||||
"integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz",
|
||||
"integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"pend": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
npm/package.json
Normal file
25
npm/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@infisical/cli",
|
||||
"private": false,
|
||||
"version": "0.0.0",
|
||||
"keywords": [
|
||||
"infisical",
|
||||
"cli",
|
||||
"command-line"
|
||||
],
|
||||
"bin": {
|
||||
"infisical": "./bin/infisical"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Infisical/infisical.git"
|
||||
},
|
||||
"author": "Infisical Inc, <daniel@infisical.com>",
|
||||
"scripts": {
|
||||
"preinstall": "node src/index.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"tar": "^6.2.0",
|
||||
"yauzl": "^3.2.0"
|
||||
}
|
||||
}
|
152
npm/src/index.cjs
Normal file
152
npm/src/index.cjs
Normal file
@ -0,0 +1,152 @@
|
||||
const childProcess = require("child_process");
|
||||
const fs = require("fs");
|
||||
const stream = require("node:stream");
|
||||
const tar = require("tar");
|
||||
const path = require("path");
|
||||
const zlib = require("zlib");
|
||||
const yauzl = require("yauzl");
|
||||
|
||||
const packageJSON = require("../package.json");
|
||||
|
||||
const supportedPlatforms = ["linux", "darwin", "win32", "freebsd", "windows"];
|
||||
const outputDir = "bin";
|
||||
|
||||
const getPlatform = () => {
|
||||
let platform = process.platform;
|
||||
|
||||
if (platform === "win32") {
|
||||
platform = "windows";
|
||||
}
|
||||
|
||||
if (!supportedPlatforms.includes(platform)) {
|
||||
console.error("Your platform doesn't seem to be of type darwin, linux or windows");
|
||||
process.exit(1);
|
||||
}
|
||||
return platform;
|
||||
};
|
||||
|
||||
const getArchitecture = () => {
|
||||
const architecture = process.arch;
|
||||
let arch = "";
|
||||
|
||||
if (architecture === "x64" || architecture === "amd64") {
|
||||
arch = "amd64";
|
||||
} else if (architecture === "arm64") {
|
||||
arch = "arm64";
|
||||
} else if (architecture === "arm") {
|
||||
// If the platform is Linux, we should find the exact ARM version, otherwise we default to armv7 which is the most common
|
||||
if (process.platform === "linux" || process.platform === "freebsd") {
|
||||
const output = childProcess.execSync("uname -m").toString().trim();
|
||||
|
||||
const armVersions = ["armv5", "armv6", "armv7"];
|
||||
|
||||
const armVersion = armVersions.find(version => output.startsWith(version));
|
||||
|
||||
if (armVersion) {
|
||||
arch = armVersion;
|
||||
} else {
|
||||
arch = "armv7";
|
||||
}
|
||||
} else {
|
||||
arch = "armv7";
|
||||
}
|
||||
} else if (architecture === "ia32") {
|
||||
arch = "i386";
|
||||
} else {
|
||||
console.error("Your architecture doesn't seem to be supported. Your architecture is", architecture);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return arch;
|
||||
};
|
||||
|
||||
async function extractZip(buffer, targetPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
yauzl.fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
zipfile.readEntry();
|
||||
zipfile.on("entry", entry => {
|
||||
const isExecutable = entry.fileName === "infisical" || entry.fileName === "infisical.exe";
|
||||
|
||||
if (/\/$/.test(entry.fileName) || !isExecutable) {
|
||||
// Directory entry
|
||||
zipfile.readEntry();
|
||||
} else {
|
||||
// File entry
|
||||
zipfile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
const outputPath = path.join(targetPath, entry.fileName.includes("infisical") ? "infisical" : entry.fileName);
|
||||
const writeStream = fs.createWriteStream(outputPath);
|
||||
|
||||
readStream.pipe(writeStream);
|
||||
writeStream.on("close", () => {
|
||||
zipfile.readEntry();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
zipfile.on("end", resolve);
|
||||
zipfile.on("error", reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const PLATFORM = getPlatform();
|
||||
const ARCH = getArchitecture();
|
||||
const NUMERIC_RELEASE_VERSION = packageJSON.version;
|
||||
const LATEST_RELEASE_VERSION = `v${NUMERIC_RELEASE_VERSION}`;
|
||||
const EXTENSION = PLATFORM === "windows" ? "zip" : "tar.gz";
|
||||
const downloadLink = `https://github.com/Infisical/infisical/releases/download/infisical-cli/${LATEST_RELEASE_VERSION}/infisical_${NUMERIC_RELEASE_VERSION}_${PLATFORM}_${ARCH}.${EXTENSION}`;
|
||||
|
||||
// Ensure the output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir);
|
||||
}
|
||||
|
||||
// Download the latest CLI binary
|
||||
try {
|
||||
const response = await fetch(downloadLink, {
|
||||
headers: {
|
||||
Accept: "application/octet-stream"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch: ${response.status} - ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (EXTENSION === "zip") {
|
||||
// For ZIP files, we need to buffer the whole thing first
|
||||
const buffer = await response.arrayBuffer();
|
||||
await extractZip(Buffer.from(buffer), outputDir);
|
||||
} else {
|
||||
// For tar.gz files, we stream
|
||||
await new Promise((resolve, reject) => {
|
||||
const outStream = stream.Readable.fromWeb(response.body)
|
||||
.pipe(zlib.createGunzip())
|
||||
.pipe(
|
||||
tar.x({
|
||||
C: path.join(outputDir),
|
||||
filter: path => path === "infisical"
|
||||
})
|
||||
);
|
||||
|
||||
outStream.on("error", reject);
|
||||
outStream.on("close", resolve);
|
||||
});
|
||||
}
|
||||
|
||||
// Give the binary execute permissions if we're not on Windows
|
||||
if (PLATFORM !== "win32") {
|
||||
fs.chmodSync(path.join(outputDir, "infisical"), "755");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error downloading or extracting Infisical CLI:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
main();
|
Reference in New Issue
Block a user