How Providers Work
A provider is the program that talks to a cloud or service API for Kite: the AWS provider creates VPCs and EC2 instances, the Files provider writes local files. The Kite engine never calls those APIs itself. It runs each provider as a separate process (the built-in Files provider is the one exception) and asks it, over gRPC, to read, create, update or delete resources.
Separate processes keep each provider's dependencies, such as a cloud SDK, out of the engine, and let you install and upgrade providers independently of the CLI. The protocol is defined in Protocol Buffers, so a provider can be written in any language that has gRPC; the SDK Kite ships today is for Java.
This page follows one provider from its line in kitefile.yml to the calls the engine makes on it. Everything before Writing a provider is for anyone who uses Kite; that last section is for provider authors.
Declare providers in kitefile.yml
A project lists the providers it needs under dependencies in kitefile.yml, in the project directory. An entry names an official provider and its version, or points at the git repository of a provider you build yourself.
dependencies:
# An official provider: kite providers install downloads its GitHub release
- name: aws
version: "0.2.0"
# Your own provider: a GitHub release of the repo, or a build of its source
- name: my-provider
git: github.com/my-org/kite-provider-mine
ref: v1.0.0
# Optional. These are the defaults.
providers:
idleTimeout: 30m
persistent: true
environments:
- name: dev
credentials:
- type: aws # sent to the provider named "aws"
profile: dev-sandbox
region: eu-west-1| Field | Meaning |
|---|---|
name | The provider name. Your Kite code imports from it ("aws/networking") and a credential's type must match it. A tf/ prefix sends the entry to the Terraform bridge. |
version | The provider version, for example "0.2.0". See Which version runs. |
git | The repository of a provider that is not one of the official ones, for example github.com/my-org/kite-provider-mine. |
ref | The release tag, branch or commit to install from git. Without it, the latest GitHub release is downloaded. |
path | The folder inside the repository that holds the provider, for repositories with several providers. |
The credentials of the environment you run (kite apply -e dev) are sent to the provider whose name matches their type; see Calls over gRPC.
Install providers
Run kite providers install in the project directory to install every provider in kitefile.yml:
kite providers install- Official providers (
aws,azure,gcp,files) come from the latest release of that provider in kitecorp/kite-providers. To install one specific release, name it:kite providers install aws@0.2.0. - A dependency with
gitis downloaded from the repository's GitHub release when there is one. Otherwise Kite clones the repository and builds it, detecting Gradle (installDist), Maven, Go, Cargo or Make. Add--from-sourceto always build.
kite plan and kite apply also try to install a declared provider that is missing before they start it.
What an installed provider looks like
Each version gets its own folder under ~/.kite/providers/<name>/, named by the bare version: 0.2.0, v0.2.0 and the release tag aws-v0.2.0 all mean the folder 0.2.0. current points to the highest version installed:
~/.kite/providers/
└── aws/
├── 0.2.0/
│ ├── provider.json
│ ├── bin/provider # launcher script (bin/provider.bat on Windows)
│ ├── lib/ # the provider's jars
│ └── schemas/
│ └── networking/
│ ├── Vpc.kite
│ └── Subnet.kite
└── current -> 0.2.0{
"name": "aws",
"version": "0.2.0",
"protocolVersion": 1,
"executable": "bin/provider"
}provider.json gives the provider's name and version, the protocol version it speaks, and the file to run. A Java provider runs through bin/provider, a launcher script that starts the jars in lib/ with the java from JAVA_HOME or your PATH, which must be Java 25 or newer. schemas/ holds one .kite file per resource type, in one folder per domain; see How provider types reach your code.
Which version runs
When the kitefile gives a version (or a git ref), the engine runs exactly that version, from ~/.kite/providers/<name>/<version>/. If that version is not installed and cannot be installed, the provider is not loaded and the run says so; it never falls back to another version.
When the kitefile gives no version, the engine runs the one current points to. Because current always points to the highest version installed, installing a newer release means the next run uses it.
Providers installed for one project with kite providers install --local live in the project's .kite/providers/ and are used before the ones in ~/.kite/providers/.
Start a provider: the handshake
When no running copy can be reused, the engine starts the provider itself:
- It runs the
executablefromprovider.jsonas a child process and adds three environment variables:KITE_PLUGIN_MAGIC_COOKIE(a random value, new for each engine run),KITE_PLUGIN_PROTOCOL_VERSION=1andKITE_PLUGIN_IDLE_TIMEOUT(the idle timeout in milliseconds). - The provider checks that the cookie is set and that it speaks protocol version 1, picks a free port and starts its gRPC server on it.
- It prints one line to standard output:
KITE_PLUGIN|1|<port>|grpc. - The engine waits up to 30 seconds for that line, checks the prefix, the protocol version and
grpc, and connects to127.0.0.1:<port>over gRPC without TLS. - It records the port and the process in
~/.kite/run/, so later runs can find the provider.
If the provider exits, prints a malformed line or stays silent for 30 seconds, the engine stops the process and reports that the provider failed to load. Other lines the provider prints to standard output are logged at debug level, and lines on standard error go to Kite's log.
Why announce the port on standard output? The provider picks a free port itself, so two providers never compete for a fixed port and nothing has to be configured. Standard output is the one channel the engine already has to a process it just started.
What the cookie is for. It tells the provider that Kite started it. Run a provider binary by hand and it refuses to start, with exit code 1:
$ build/install/my-provider/bin/provider
This is a Kite provider plugin. It must be launched by the Kite engine.Reuse a running provider
Starting a provider takes time (a Java provider starts a JVM), so Kite keeps providers running between commands. When a run ends, the engine closes its connection but leaves the provider process running, and the next kite plan or kite apply connects to it instead of starting a new one.
Each running provider has three files in ~/.kite/run/, named after its name and version:
~/.kite/run/aws@0.2.0.port the gRPC port, for example 62901
~/.kite/run/aws@0.2.0.pid the process id and the time it started
~/.kite/run/aws@0.2.0.activity when the provider was last used (epoch milliseconds)A run uses them to decide whether it can reuse the provider:
The .pid file holds the process start time as well as its id because operating systems reuse process ids: another process with the same id has a different start time, so Kite does not mistake it for the provider. Recent activity alone never skips the health check; the recorded process must also be alive.
Idle timeout and settings
A provider stops itself once the idle timeout passes without a gRPC call. It checks every 30 seconds, or every half timeout when the timeout is shorter than a minute. Change the timeout, or stop providers at the end of every command, in kitefile.yml:
providers:
idleTimeout: 10m # default 30m; also accepts s, h, ms, or a plain number of seconds
persistent: false # stop providers when each command ends (default true)With persistent: false the engine neither records nor reuses providers.
Manage running providers
| Command | What it does |
|---|---|
kite providers status | Lists the running providers with their port, uptime and idle time, from a HealthCheck call to each. |
kite providers stop aws | Sends StopProvider to the running aws provider and deletes its run files. --all stops every running provider. |
kite providers start aws | Starts the provider ahead of time, so the next command does not wait for it. |
Calls over gRPC
Every provider implements the same Provider gRPC service, defined in provider.proto.
| RPC | When the engine calls it |
|---|---|
GetProviderSchema | When it loads a provider that has no schemas/ folder, to learn its resource types. Always for the Terraform bridge. |
ConfigureProvider | Once per provider in each run, with the credentials of the selected environment whose type matches the provider name. |
ReadResource | To read the current state of a resource from the cloud. |
ImportResource | To adopt an existing cloud resource by its id, such as an instance id, ARN or name. |
CreateResource, UpdateResource, DeleteResource | When kite apply or kite destroy changes resources. |
HealthCheck | Before reusing a running provider that is not provably alive and recently used, and for kite providers status. |
StopProvider | For kite providers stop. |
ValidateResourceConfig, PlanResourceChange | Part of the protocol and implemented by the Java SDK. The engine does not call them yet. |
Resource data travels as msgpack
Resource properties are not protobuf messages. Requests and responses carry them in a generic ResourcePayload, whose msgpack field holds the properties encoded with MessagePack:
message ResourcePayload {
bytes msgpack = 1; // the resource's properties, MessagePack-encoded
bytes json = 2; // alternative encoding, for debugging
}That is why one protocol serves every resource type: adding a resource type to a provider needs no new protobuf definitions and no generated stubs. The Java SDK converts the payload to and from your own resource class.
Responses can also return privateData, bytes that only the provider understands. The engine stores them with the resource's state and sends them back on the next call for that resource. Errors and warnings come back as a list of diagnostics in the response.
How provider types reach your code
A .kite file can use a provider's resource types only after importing them, by provider name and domain:
import Vpc, Subnet from "aws/networking"
resource Vpc main {
cidrBlock = "10.0.0.0/16"
}
resource Subnet app {
vpcId = main.vpcId
cidrBlock = "10.0.1.0/24"
}The provider name is the name from kitefile.yml, and the domain (networking above) is a folder in the provider's schemas/. If the provider has no types in that domain, Kite looks in all of its domains.
The engine builds the list of types when it loads the providers, before it parses your code:
- If the provider folder has
schemas/, it reads everyschemas/<domain>/<Type>.kitefile. No call to the provider is needed, and the files carry the default values of properties. - Otherwise it calls
GetProviderSchemaand puts every type in the domaingeneral.
Importing from aws when the provider did not load fails with a message that starts Provider 'aws' is not installed or failed to load. Importing a type the provider does not have fails with the list of types it does have.
Two special cases
The Files provider runs inside the engine
The Files provider, which manages local files, is built into the engine. It runs as a plain Java object in the engine's own process: there is nothing to install, no process to start and no handshake. Its type is available as import File from "files".
Terraform providers run through the bridge
A dependency whose name starts with tf/, such as tf/aws, is not installed like other providers. The engine starts the terraform-bridge provider, which must already be installed in ~/.kite/providers/terraform-bridge/, and passes the Terraform provider's name in KITE_TF_PROVIDER_NAME. The bridge starts the Terraform provider binary given by KITE_TF_PROVIDER_PATH, which Kite does not set yet, so it must be in your environment. The bridge runs that binary as its own child process and translates between Kite's protocol and Terraform's.
The bridge has no schemas/ folder. On every start the engine calls GetProviderSchema, and the bridge answers with the types it read from the Terraform provider. See Terraform Bridge for writing Kite code with these types.
Writing a provider
A Java provider has three parts: a class for each resource type, a handler with the create, read, update and delete operations, and a main class that starts the gRPC server. It builds with JDK 25, the provider SDK's minimum. For a complete, working provider, see grpc-example in kitecorp/kite-providers.
1. The resource class
A plain Java class. @TypeName sets the type name used in Kite code, each @Property field becomes a property, and @Cloud marks the properties that the provider sets and users cannot.
package com.example.provider;
import cloud.kitelang.api.annotations.Cloud;
import cloud.kitelang.api.annotations.Property;
import cloud.kitelang.api.annotations.TypeName;
import lombok.Data;
@Data
@TypeName("File")
public class FileResource {
@Property
private String path;
@Property
private String content;
@Cloud // set by the provider, never by the user
@Property
private String lastModified;
}2. The handler
Extend ResourceTypeHandler<T> and implement the four operations. The SDK turns each gRPC call into a call to one of these methods, with the payload already converted to your class, and converts what you return back to msgpack. Return null from read when the resource no longer exists. An exception thrown from a method is sent to the engine as an error diagnostic.
package com.example.provider;
import cloud.kitelang.provider.ResourceTypeHandler;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileResourceType extends ResourceTypeHandler<FileResource> {
@Override
public FileResource create(FileResource resource) {
return write(resource);
}
@Override
public FileResource read(FileResource resource) {
var path = Path.of(resource.getPath());
if (!Files.exists(path)) {
return null; // the resource no longer exists
}
try {
resource.setContent(Files.readString(path));
resource.setLastModified(Files.getLastModifiedTime(path).toString());
return resource;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public FileResource update(FileResource resource) {
return write(resource);
}
@Override
public boolean delete(FileResource resource) {
try {
return Files.deleteIfExists(Path.of(resource.getPath()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private FileResource write(FileResource resource) {
try {
var path = Path.of(resource.getPath());
Files.writeString(path, resource.getContent());
resource.setLastModified(Files.getLastModifiedTime(path).toString());
return resource;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}3. The main class
Extend KiteProvider and pass an instance to ProviderServer.serve, which performs the handshake described above. KiteProvider finds your handlers by itself: the SDK's annotation processor lists them in META-INF/kite/resource-types.txt when the code compiles.
package com.example.provider;
import cloud.kitelang.provider.KiteProvider;
import cloud.kitelang.provider.ProviderServer;
public class MyProvider extends KiteProvider {
public static void main(String[] args) throws Exception {
ProviderServer.serve(new MyProvider());
}
}Build it with the Gradle plugin
The cloud.kitelang.provider Gradle plugin, published on the Gradle Plugin Portal, turns the project into an installable provider: it applies the Java, Application and Shadow plugins, finds the main class, writes provider.json and generates the schemas. It does not add the SDK; your build depends on it, as below. Version 0.6.0 is built for Java 25, so Gradle must be 9.1 or newer and run on JDK 25.
plugins {
id 'cloud.kitelang.provider' version '0.6.0'
}
version = '0.1.0'
repositories {
mavenCentral()
}
kiteProvider {
name = 'my-provider'
}
dependencies {
implementation 'cloud.kitelang:kite-provider-sdk:0.4.0'
annotationProcessor 'cloud.kitelang:kite-provider-sdk:0.4.0'
compileOnly 'org.projectlombok:lombok:1.18.44'
annotationProcessor 'org.projectlombok:lombok:1.18.44'
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}The SDK comes from Maven Central as cloud.kitelang:kite-provider-sdk and brings cloud.kitelang:kite-api (the @TypeName, @Property and @Cloud annotations) and cloud.kitelang:kite-proto (the gRPC protocol) with it. It is listed twice: implementation compiles your code against it, and annotationProcessor runs its annotation processor, which writes the META-INF/kite/resource-types.txt that KiteProvider finds your handlers in. The SDK is compiled for Java 25, so the toolchain must be 25 or newer.
Which Kite can run it: a provider built with SDK 0.4.0 refuses every call that does not carry its cookie (see the handshake). Kite 0.4.1 and older do not send it, so they cannot use the provider.
Settings in the kiteProvider block:
| Setting | Default | Meaning |
|---|---|---|
name | project name | Provider name written to provider.json. |
mainClass | detected | The class with the main method; set it only when detection fails. |
protocolVersion | 1 | Protocol version written to provider.json. |
logoUrl | none | Logo URL written to the provider.json inside the jar. |
docsEnabled | true | Registers generateProviderDocs and generates the schemas that the distribution ships. |
docsFormats | html,markdown,schemas | Formats generateProviderDocs writes. |
docsOutputDir | docs | Folder generateProviderDocs writes to, relative to the project. |
The tasks you use:
| Task | What it does |
|---|---|
installDist | Builds build/install/<name>/: the same layout as an installed provider. |
installGlobal | Runs installDist, copies the result to ~/.kite/providers/<name>/<name>-v<version>/ and points current at it, so Kite runs your local build. |
generateProviderSchemas | Generates the .kite schemas under build/. installDist runs it and ships its output. |
generateProviderDocs | Writes HTML, Markdown and schema documentation into docsOutputDir, a docs/ folder meant to be committed. It runs only when you call it; build and installDist never touch docs/. The kite-providers release workflow calls it when it publishes a version. |
For the example above, ./gradlew installDist produces:
build/install/my-provider/
├── bin/
│ ├── provider
│ └── provider.bat
├── lib/
├── provider.json
└── schemas/
└── general/
└── File.kite// File
schema File {
string path
string content
@cloud
string lastModified
}A type's schema goes in the folder of its domain: the last part of its handler's package name when that is one of networking, compute, storage, dns, loadbalancing, database, security, iam, monitoring, core, container or files, and general otherwise. That folder name is the domain users import from, as in "aws/networking".
To check the handshake without Kite, set the two variables yourself:
$ ./gradlew installDist
$ KITE_PLUGIN_MAGIC_COOKIE=test KITE_PLUGIN_PROTOCOL_VERSION=1 build/install/my-provider/bin/provider
KITE_PLUGIN|1|50676|grpcSee also: Architecture for where providers sit in Kite, Imports for the import system, and the provider registry for the resource types of each official provider.