Skip to main content
Documentation

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.

Provider lifecycleDiagram
Each run repeats these steps. A provider that is already running is reused instead of started.

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.

kitefile.yml
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
FieldMeaning
nameThe 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.
versionThe provider version, for example "0.2.0". See Which version runs.
gitThe repository of a provider that is not one of the official ones, for example github.com/my-org/kite-provider-mine.
refThe release tag, branch or commit to install from git. Without it, the latest GitHub release is downloaded.
pathThe 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 git is 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-source to 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
provider.json
{
    "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:

  1. It runs the executable from provider.json as a child process and adds three environment variables: KITE_PLUGIN_MAGIC_COOKIE (a random value, new for each engine run), KITE_PLUGIN_PROTOCOL_VERSION=1 and KITE_PLUGIN_IDLE_TIMEOUT (the idle timeout in milliseconds).
  2. 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.
  3. It prints one line to standard output: KITE_PLUGIN|1|<port>|grpc.
  4. The engine waits up to 30 seconds for that line, checks the prefix, the protocol version and grpc, and connects to 127.0.0.1:<port> over gRPC without TLS.
  5. It records the port and the process in ~/.kite/run/, so later runs can find the provider.
Handshake and first callsDiagram
A new provider process announces its port on standard output; everything after that is gRPC.

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:

Reuse or startDiagram
A provider is reused only when its process is provably alive or answers a health check.

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:

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

CommandWhat it does
kite providers statusLists the running providers with their port, uptime and idle time, from a HealthCheck call to each.
kite providers stop awsSends StopProvider to the running aws provider and deletes its run files. --all stops every running provider.
kite providers start awsStarts 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.

RPCWhen the engine calls it
GetProviderSchemaWhen it loads a provider that has no schemas/ folder, to learn its resource types. Always for the Terraform bridge.
ConfigureProviderOnce per provider in each run, with the credentials of the selected environment whose type matches the provider name.
ReadResourceTo read the current state of a resource from the cloud.
ImportResourceTo adopt an existing cloud resource by its id, such as an instance id, ARN or name.
CreateResource, UpdateResource, DeleteResourceWhen kite apply or kite destroy changes resources.
HealthCheckBefore reusing a running provider that is not provably alive and recently used, and for kite providers status.
StopProviderFor kite providers stop.
ValidateResourceConfig, PlanResourceChangePart 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:

kite/v1/payload.proto
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 every schemas/<domain>/<Type>.kite file. No call to the provider is needed, and the files carry the default values of properties.
  • Otherwise it calls GetProviderSchema and puts every type in the domain general.

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.

FileResource.java
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.

FileResourceType.java
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.

MyProvider.java
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.

build.gradle
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:

SettingDefaultMeaning
nameproject nameProvider name written to provider.json.
mainClassdetectedThe class with the main method; set it only when detection fails.
protocolVersion1Protocol version written to provider.json.
logoUrlnoneLogo URL written to the provider.json inside the jar.
docsEnabledtrueRegisters generateProviderDocs and generates the schemas that the distribution ships.
docsFormatshtml,markdown,schemasFormats generateProviderDocs writes.
docsOutputDirdocsFolder generateProviderDocs writes to, relative to the project.

The tasks you use:

TaskWhat it does
installDistBuilds build/install/<name>/: the same layout as an installed provider.
installGlobalRuns installDist, copies the result to ~/.kite/providers/<name>/<name>-v<version>/ and points current at it, so Kite runs your local build.
generateProviderSchemasGenerates the .kite schemas under build/. installDist runs it and ships its output.
generateProviderDocsWrites 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
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|grpc

See 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.