Local development
This guide will help you contribute to the Infisical SDK.
Fork and clone the repo
Fork the repository to your own GitHub account and then clone it to your local device.
Once, you’ve done that, create a new branch:
Set up environment variables
Start by creating a .env file at the root of the Infisical directory then copy the contents of the file below into the .env file.
The above values are required for running tests locally. Before opening a pull request, make sure to run cargo test
to ensure that all tests pass.
Guidelines
Predictable and consistent
When adding new functionality (such as new functions), it’s very important that the functionality is added to all the SDK’s. This is to ensure that the SDK’s are predictable and consistent across all languages. If you are adding new functionality, please make sure to add it to all the SDK’s.
Handling errors
Error handling is very important when writing SDK’s. We want to make sure that the SDK’s are easy to use, and that the user gets a good understanding of what went wrong when something fails. When adding new functionality, please make sure to add proper error handling. Read more about error handling here.
Tests
If you add new functionality or modify existing functionality, please write tests thats properly cover the new functionality. You can run tests locally by running cargo test
from the root directory. You must always run tests before opening a pull request.
Code style
Please follow the default rust styling guide when writing code for the base SDK. Read more about rust code style here.
Prerequisites for contributing
Understanding the terms
In the guide we use some terms that might be unfamiliar to you. Here’s a quick explanation of the terms we use:
- Base SDK: The base SDK is the SDK that all other SDK’s are built on top of. The base SDK is written in Rust, and is responsible for executing commands and parsing the input and output to and from JSON.
- Commands: Commands are what’s being sent from the target language to the command handler. The command handler uses the command to execute the corresponding function in the base SDK. Commands are in reality just a JSON string that tells the command handler what function to execute, and what input to use.
- Command handler: The command handler is the part of the base SDK that takes care of executing commands. It also takes care of parsing the input and output to and from JSON.
- Target language: The target language refers to the actual SDK code. For example, the Node.js SDK is a “target language”, and so is the Python SDK.
Understanding the execution flow
After the target language SDK is initiated, it uses language-specific bindings to interact with the base SDK. These bindings are instantiated, setting up the interface for command execution. A client within the command handler is created, which issues commands to the base SDK. When a command is executed, it is first validated. If valid, the command handler locates the corresponding command to perform. If the command executes successfully, the command handler returns the output to the target language SDK, where it is parsed and returned to the user. If the command handler fails to validate the input, an error will be returned to the target language SDK.
Execution flow diagram for the SDK from the target language to the base SDK. The execution flow is the same for all target languages.
Rust knowledge
Contributing to the SDK requires intermediate to advanced knowledge of Rust concepts such as lifetimes, traits, generics, and async/await (futures), and more.
Rust setup
The base SDK is written in rust. Therefore you must have rustc and cargo installed. You can install rustc and cargo by following the instructions here.
You shouldn’t have to use the rust cross compilation toolchain, as all compilation is done through a collection of Github Actions. However. If you need to test cross compilation, please do so with Github Actions.
Tests
If you add new functionality or modify existing functionality, please write tests thats properly cover the new functionality. You can run tests locally by running cargo test
from the root directory.
Language-specific crates
The language-specific crates should ideally never have to be modified, as they are simply a wrapper for the infisical-json
crate, which executes “commands” from the base SDK. If you need to create a new target-language specific crate, please try to create native bindings for the target language. Some languages don’t have direct support for native bindings (Java as an example). In those cases we can use the C bindings (crates/infisical-c
) in the target language.
Generate types
Having almost seemless type safety from the base SDK to the target language is critical, as writing types for each language has a lot of drawbacks such as duplicated code, and lots of overhead trying to keep the types up-to-date and in sync across a large collection of languages. Therefore we decided to use QuickType and Serde to help us generate types for each language. In our Rust base SDK (crates/infisical
), we define all the inputs/outputs.
If you are interested in reading about QuickType works under the hood, you can read more here.
This is an example of a type defined in Rust (both input and output). For this to become a generated type, you’ll need to add it to our schema generator. More on that further down.
Adding input types to the schema generator
You will only have to define outputs in our schema generator, then QuickType will take care of the rest behind the scenes. You can find the Rust crate that takes care of type generation here: crates/sdk-schemas/src/main.rs
.
Simply add the output (also called response), to the write_schema_for_response!
macro. This will let QuickType know that it should generate types for the given structs. The main function will look something like this:
Generating the types for the target language
Once you’ve added the output to the schema generator, you can generate the types for the target language by running the following command from the root directory:
This command will run the schemas.ts
file found in the support/scripts
folder. If you are adding a new language, it’s important that you add the language to the code.
This is an example of how how we generate types for Node.js:
Building bindings
We’ve tried to streamline the building process as much as possible. So you shouldn’t have to worry much about building bindings, as it should just be a few commands.
Node.js
Building bindings for Node.js is very straight foward. The command below will generate NAPI bindings for Node.js, and move the bindings to the correct folder. We use NAPI-RS to generate the bindings.
Python
To generate and use python bindings you will need to run the following commands. The Python SDK is located inside the crates folder. This is a limitation of the maturin tool, forcing us to structure the project in this way.
After running the commands above, it’s very important that you rename the generated .so file to infisical_py.so
. After renaming it you also need to move it into the root of the crates/infisical-py
folder.
Java
Java uses the C bindings to interact with the base SDK. To build and use the C bindings in Java, please follow the instructions below.
After generating the C bindings, the generated .so or .dll has been created in the /target
directory at the root of the project.
You have to manually move the generated file into the languages/java/src/main/resources
directory.
Error handling
Error handling in the base SDK
The base SDK should never panic. If an error occurs, we should return a Result
with an error message. We have a custom Result type defined in the error.rs
file in the base SDK.
All our errors are defined in an enum called Error
. The Error
enum is defined in the error.rs
file in the base SDK. The Error
enum is used in the Result
type, which is used as the return type for all functions in the base SDK.
Returning an error
You can find many examples of how we return errors in the SDK code. A relevant example is for creating secrets, which can be found in crates/infisical/src/api/secrets/create_secret.rs
. When the error happened due to a request error to our API, we have an API error handler. This prevents duplicate code and keeps error handling consistent across the SDK. You can find the api error handler in the error.rs
file.
Error handling in the target language SDK’s.
All data sent to the target language SDK has the same format. The format is an object with 3 fields: success (boolean)
, data (could be anything or nothing)
, and errorMessage (string or null)
.
The success
field is used to determine if the request was successful or not. The data
field is used to return data from the SDK. The errorMessage
field is used to return an error message if the request was not successful.
This means that if the success if false or if the error message is not null, something went wrong and we should throw an error on the target-language level, with the error message.
Command handler
What is the command handler
The command handler (the infisical-json
crate), takes care of executing commands sent from the target language. It also takes care of parsing the input and output to and from JSON. The command handler is the only part of the base SDK that should be aware of JSON. The rest of the base SDK should be completely unaware of JSON, and only work with the Rust structs defined in the base SDK.
The command handler exposes a function called run_command
, which is what we use in the target language to execute commands. The function takes a json string as input, and returns a json string as output. We use helper functions generated by QuickType to convert the input and output to and from JSON.
Creating new SDK methods
Creating new commands is necessary when adding new methods to the SDK’s. Defining a new command is a 3-step process in most cases.
1. Define the input and output structs
Earlier in this guide, we defined the input and output structs for the CreateSecret
command. We will use that as an example here as well.
2. Creating the method in the base SDK
The first step is to create the method in the base SDK. This step will be different depending on what method you are adding. In this example we’re going to assume you’re adding a function for creating a new secret.
After you created the function for creating the secret, you’ll need need to add it to the ClientSecrets implementation. We do it this way to keep the code organized and easy to read. The ClientSecrets struct is located in the crates/infisical/src/manager/secrets.rs
file.
3. Define a new command
We define new commands in the crates/infisical-json/src/command.rs
file. The Command
enum is what we use to define new commands.
In the codesnippet below we define a new command called CreateSecret
. The CreateSecret
command takes a CreateSecretOptions
struct as input. We don’t have to define the output, because QuickType’s converter helps us with figuring out the return type for each command.
4. Add the command to the command handler
After defining the command, we need to add it to the command handler itself. This takes place in the crates/infisical-json/src/client.rs
file. The run_command
function is what we use to execute commands.
In the Client implementation we try to parse the JSON string into a Command
enum. If the parsing is successful, we match the command and execute the corresponding function.
5. Implementing the new command in the target language SDK’s
We did it! We’ve now added a new command to the base SDK. The last step is to implement the new command in the target language SDK’s. The process is a little different from language to language, but in this example we’re going to assume that we’re adding a new command to the Node.js SDK.
First you’ll need to generate the new type schemas, we added a new command, input struct, and output struct. Read more about generating types here.
Secondly you need to build the new node bindings so we can use the new functionality in the Node.js SDK. You can do this by running the following command from the languages/node
directory:
The build command will execute a build script in the infisical-napi
crate, and move the generated bindings to the appropriate folder.
After building the new bindings, you can access the new functionality in the Node.js SDK source.
And that’s it! We’ve now added a new command to the base SDK, and implemented it in the Node.js SDK. The process is very similar for all other languages, but the code will look a little different.
Conclusion
The SDK has a lot of moving parts, and it can be a little overwhelming at first. But once you get the hang of it, it’s actually quite simple. If you have any questions, feel free to reach out to us on Slack, or open an issue on GitHub.
Was this page helpful?