# SDKs

> Octelium documentation. Canonical page: <https://octelium.com/docs/octelium/latest/management/guide/sdk>.

Octelium APIs are all developed over [gRPC](https://grpc.io/). Most `octeliumctl` commands are wrappers over the *Cluster*'s *Core* API methods. The SDK clients make it easy for applications to authenticate to the *Cluster* and manage resources such as *Users*, *Services* and *Sessions* programmatically.

The examples below create an authenticated client, list *Users*, create and update a *User*, create a *Service* and list *Sessions*. The authenticated *User* must have a *Policy* that permits these operations. Read more about authentication-token *Credentials* [here](https://octelium.com/docs/octelium/latest/management/core/credential.md#authentication-tokens) and *Policies* [here](https://octelium.com/docs/octelium/latest/management/core/policy.md).

## Golang SDK

The `octelium-go` library provides an authenticated gRPC connection for the *Cluster* APIs. It also handles obtaining and refreshing the *Session* access token used by the connection.

Install the module in your Go application as follows:

```bash
go get github.com/octelium/octelium/octelium-go
```

Here is a complete management example:

```go
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/octelium/octelium/apis/main/corev1"
	"github.com/octelium/octelium/apis/main/metav1"
	octelium "github.com/octelium/octelium/octelium-go"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	if err := run(ctx); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run(ctx context.Context) error {
	domain := os.Getenv("OCTELIUM_DOMAIN")
	authToken := os.Getenv("OCTELIUM_AUTH_TOKEN")
	if domain == "" || authToken == "" {
		return fmt.Errorf("OCTELIUM_DOMAIN and OCTELIUM_AUTH_TOKEN must be set")
	}

	client, err := octelium.New(ctx,
		octelium.WithDomain(domain),
		octelium.WithAuthenticator(
			octelium.AuthenticationToken(authToken),
		),
	)
	if err != nil {
		return err
	}
	defer client.Close()

	conn, err := client.Conn(ctx)
	if err != nil {
		return err
	}
	coreClient := corev1.NewMainServiceClient(conn)

	users, err := coreClient.ListUser(ctx, &corev1.ListUserOptions{})
	if err != nil {
		return err
	}
	fmt.Printf("Cluster has %d users\n", len(users.Items))

	userName := "linus"
	user, err := coreClient.CreateUser(ctx, &corev1.User{
		Metadata: &metav1.Metadata{Name: userName},
		Spec: &corev1.User_Spec{
			Type:  corev1.User_Spec_HUMAN,
			Email: "linus@example.com",
		},
	})
	if err != nil {
		return err
	}

	user.Spec.Email = "linus.updated@example.com"
	user, err = coreClient.UpdateUser(ctx, user)
	if err != nil {
		return err
	}
	fmt.Printf("Updated %q email to %s\n", user.Metadata.Name, user.Spec.Email)

	service, err := coreClient.CreateService(ctx, &corev1.Service{
		Metadata: &metav1.Metadata{Name: "example-svc"},
		Spec: &corev1.Service_Spec{
			Mode:     corev1.Service_Spec_HTTP,
			Port:     443,
			IsPublic: true,
			Config: &corev1.Service_Spec_Config{
				Upstream: &corev1.Service_Spec_Config_Upstream{
					Type: &corev1.Service_Spec_Config_Upstream_Url{
						Url: "https://example.com",
					},
				},
			},
		},
	})
	if err != nil {
		return err
	}
	fmt.Printf("Created service %q\n", service.Metadata.Name)

	sessions, err := coreClient.ListSession(ctx, &corev1.ListSessionOptions{})
	if err != nil {
		return err
	}
	for _, session := range sessions.Items {
		fmt.Printf("Session %q is present\n", session.Metadata.Name)
	}

	return nil
}
```

Set `OCTELIUM_DOMAIN` and `OCTELIUM_AUTH_TOKEN` before running the program.

## TypeScript SDK

The TypeScript/Node.js SDK is provided by `@octelium/sdk`:

```bash
npm i @octelium/sdk
```

The generated protobuf messages expose `create` factories, which fill in their default fields and make them ready for API calls. Here is the equivalent management example:

```typescript
import { OcteliumClient } from "@octelium/sdk";
import {
  GetOptions,
  Metadata,
} from "@octelium/apis/main/metav1";
import {
  Service,
  Service_Spec,
  Service_Spec_Config,
  Service_Spec_Config_Upstream,
  Service_Spec_Mode,
  User,
  User_Spec,
  User_Spec_Type,
} from "@octelium/apis/main/corev1";

const domain = process.env.OCTELIUM_DOMAIN;
const authToken = process.env.OCTELIUM_AUTH_TOKEN;

if (!domain || !authToken) {
  throw new Error("OCTELIUM_DOMAIN and OCTELIUM_AUTH_TOKEN must be set");
}

const client = await OcteliumClient.create({
  domain,
  auth: {
    type: "authToken",
    authToken: { token: authToken },
  },
});

const users = await client.coreV1.listUser({});
console.log("Cluster has", users.response.items.length, "users");

const userName = "linus";
const createdUser = await client.coreV1.createUser(User.create({
  metadata: Metadata.create({ name: userName }),
  spec: User_Spec.create({
    type: User_Spec_Type.HUMAN,
    email: "linus@example.com",
  }),
}));
console.log("Created user", createdUser.response.metadata?.name);

const userResponse = await client.coreV1.getUser(GetOptions.create({ name: userName }));
const user = userResponse.response;
user.spec!.email = "linus.updated@example.com";
const updatedUser = await client.coreV1.updateUser(user);
console.log("Updated email to", updatedUser.response.spec?.email);

const service = await client.coreV1.createService(Service.create({
  metadata: Metadata.create({ name: "example-svc" }),
  spec: Service_Spec.create({
    mode: Service_Spec_Mode.HTTP,
    port: 443,
    isPublic: true,
    config: Service_Spec_Config.create({
      upstream: Service_Spec_Config_Upstream.create({
        type: { oneofKind: "url", url: "https://example.com" },
      }),
    }),
  }),
}));
console.log("Created service", service.response.metadata?.name);

const sessions = await client.coreV1.listSession({});
for (const session of sessions.response.items) {
  console.log("Session", session.metadata?.name, "is present");
}

```

The TypeScript example uses the same `OCTELIUM_DOMAIN` and `OCTELIUM_AUTH_TOKEN` environment variables as the Go example.

## Python SDK

The Python SDK is provided by `octelium-sdk` and uses the generated API messages from `octelium-apis`:

```bash
pip install octelium-sdk
pip install octelium-apis
```

Here is the equivalent asynchronous management example:

```python
import asyncio
import os

from octelium.api.main.core.v1 import (
    ListSessionOptions,
    ListUserOptions,
    MainServiceStub,
    Service,
    ServiceSpec,
    ServiceSpecConfig,
    ServiceSpecConfigUpstream,
    ServiceSpecMode,
    User,
    UserSpec,
    UserSpecType,
)
from octelium.api.main.meta.v1 import Metadata
from octelium.sdk import AuthConfig, AuthTokenConfig, OcteliumClient, OcteliumClientConfig


async def main() -> None:
    domain = os.environ.get("OCTELIUM_DOMAIN")
    auth_token = os.environ.get("OCTELIUM_AUTH_TOKEN")
    if not domain or not auth_token:
        raise RuntimeError("OCTELIUM_DOMAIN and OCTELIUM_AUTH_TOKEN must be set")

    async with await OcteliumClient.create(
        OcteliumClientConfig(
            domain=domain,
            auth=AuthConfig(
                type="auth_token",
                auth_token=AuthTokenConfig(token=auth_token),
            ),
        )
    ) as client:
        core_client: MainServiceStub = client.core_v1

        users = await core_client.list_user(ListUserOptions())
        print(f"Cluster has {len(users.items)} users")

        user_name = "linus"
        user = await core_client.create_user(
            User(
                metadata=Metadata(name=user_name),
                spec=UserSpec(
                    type=UserSpecType.HUMAN,
                    email="linus@example.com",
                ),
            )
        )

        user.spec.email = "linus.updated@example.com"
        user = await core_client.update_user(user)
        print(f"Updated {user.metadata.name!r} email to {user.spec.email}")

        service = await core_client.create_service(
            Service(
                metadata=Metadata(name="example-svc"),
                spec=ServiceSpec(
                    mode=ServiceSpecMode.HTTP,
                    port=443,
                    is_public=True,
                    config=ServiceSpecConfig(
                        upstream=ServiceSpecConfigUpstream(
                            url="https://example.com",
                        ),
                    ),
                ),
            )
        )
        print(f"Created service {service.metadata.name!r}")

        sessions = await core_client.list_session(ListSessionOptions())
        for session in sessions.items:
            print(f"Session {session.metadata.name!r} is present")

if __name__ == "__main__":
    asyncio.run(main())
```

The Python example uses the same `OCTELIUM_DOMAIN` and `OCTELIUM_AUTH_TOKEN` environment variables as the other examples.

## Rust SDK

The Rust SDK is provided by the `octelium` crate. It re-exports the generated API messages and [tonic](https://github.com/hyperium/tonic) gRPC clients under `octelium::apis`, obtains and refreshes the *Session* access token and hands out authenticated clients for the *Cluster* APIs:

```bash
cargo add octelium
cargo add tokio --features macros,rt-multi-thread
```

Here is the equivalent management example:

```rust
use octelium::apis::{corev1, metav1};
use octelium::{AuthenticationToken, Client};

use corev1::service::spec::config::{Upstream, upstream};
use corev1::service::spec::{Config, Mode};
use corev1::user::spec::Type;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (Ok(domain), Ok(auth_token)) = (
        std::env::var("OCTELIUM_DOMAIN"),
        std::env::var("OCTELIUM_AUTH_TOKEN"),
    ) else {
        return Err("OCTELIUM_DOMAIN and OCTELIUM_AUTH_TOKEN must be set".into());
    };

    let client = Client::builder()
        .domain(domain)
        .authenticator(AuthenticationToken::new(auth_token))
        .build()
        .await?;

    let mut core_client = client.core_v1();

    let users = core_client
        .list_user(corev1::ListUserOptions::default())
        .await?
        .into_inner();
    println!("Cluster has {} users", users.items.len());

    let mut user = core_client
        .create_user(corev1::User {
            metadata: Some(metav1::Metadata {
                name: "linus".into(),
                ..Default::default()
            }),
            spec: Some(corev1::user::Spec {
                r#type: Type::Human.into(),
                email: "linus@example.com".into(),
                ..Default::default()
            }),
            ..Default::default()
        })
        .await?
        .into_inner();

    user.spec.get_or_insert_default().email = "linus.updated@example.com".into();
    let user = core_client.update_user(user).await?.into_inner();
    println!(
        "Updated {:?} email to {}",
        user.metadata.unwrap_or_default().name,
        user.spec.unwrap_or_default().email
    );

    let service = core_client
        .create_service(corev1::Service {
            metadata: Some(metav1::Metadata {
                name: "example-svc".into(),
                ..Default::default()
            }),
            spec: Some(corev1::service::Spec {
                mode: Mode::Http.into(),
                port: 443,
                is_public: true,
                config: Some(Config {
                    upstream: Some(Upstream {
                        r#type: Some(upstream::Type::Url("https://example.com".into())),
                        ..Default::default()
                    }),
                    ..Default::default()
                }),
                ..Default::default()
            }),
            ..Default::default()
        })
        .await?
        .into_inner();
    println!(
        "Created service {:?}",
        service.metadata.unwrap_or_default().name
    );

    let sessions = core_client
        .list_session(corev1::ListSessionOptions::default())
        .await?
        .into_inner();
    for session in sessions.items {
        println!(
            "Session {:?} is present",
            session.metadata.unwrap_or_default().name
        );
    }

    Ok(())
}
```

Protobuf enum fields are generated as `i32` values, so enums such as `Type::Human` are converted with `.into()`. The Rust example uses the same `OCTELIUM_DOMAIN` and `OCTELIUM_AUTH_TOKEN` environment variables as the other examples. You can also use `Client::from_env()` to read the *Cluster* domain and the *Credential* directly from these environment variables.
