Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controller info endpoint and API integration examples #75

Merged
merged 6 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions IntegrationGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,179 @@ You can run `orchard dev` locally and navigate to http://127.0.0.1:6120/v1/ for

![](docs/orchard-api-documentation-browser.png)

## Using the API

Below you'll find examples of using Orchard API via vanilla Python's request library and Golang package that Orchard CLI build on top of.

### Authentication

When running in non-development mode, Orchard API expects a [basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) to be provided for each API call.

Below you'll find two snippets that retrieve controller's information and output its version:

**Python**

```python
import requests
from requests.auth import HTTPBasicAuth


def main():
# Authentication
basic_auth = HTTPBasicAuth("service account name", "service account token")

response = requests.get("http://127.0.0.1:6120/v1/info", auth=basic_auth)

print(response.json()["version"])


if __name__ == '__main__':
main()
```

**Golang**

```go
package main

import (
"context"
"fmt"
"github.com/cirruslabs/orchard/pkg/client"
"log"
)

func main() {
client, err := client.New()
if err != nil {
log.Fatalf("failed to initialize Orchard API client: %v", err)
}

controllerInfo, err := client.Controller().Info(context.Background())
if err != nil {
log.Fatalf("failed to retrieve controller's information: %v", err)
}

fmt.Println(controllerInfo.Version)
}
```

Note that we don't provide any credentials for Golang's version of the snippet: this is because Orchard's Golang API client (`github.com/cirruslabs/orchard/pkg/client`) has the ability to read the current's user Orchard context automatically.

### Creating a VM

A more intricate example would be spinning off a VM with a startup script that outputs date, reading its logs and removing it from the controller:

**Python**

```python
import time
import uuid

import requests
from requests.auth import HTTPBasicAuth


def main():
vm_name = str(uuid.uuid4())

basic_auth = HTTPBasicAuth("service account name", "service account token")

# Create VM
response = requests.post("http://127.0.0.1:6120/v1/vms", auth=basic_auth, json={
"name": vm_name,
"image": "ghcr.io/cirruslabs/macos-ventura-base:latest",
"cpu": 4,
"memory": 4096,
"startup_script": {
"script_content": "date",
}
})
response.raise_for_status()

# Retrieve VM's logs
while True:
response = requests.get(f"http://127.0.0.1:6120/v1/vms/{vm_name}/events", auth=basic_auth)
response.raise_for_status()

result = response.json()

if isinstance(result, list) and len(result) != 0:
print(result[0]["payload"])
break

time.sleep(1)

# Delete VM
response = requests.delete(f"http://127.0.0.1:6120/v1/vms/{vm_name}", auth=basic_auth)
response.raise_for_status()


if __name__ == '__main__':
main()
```

**Golang**

```go
package main

import (
"context"
"fmt"
"github.com/cirruslabs/orchard/pkg/client"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/google/uuid"
"log"
"time"
)

func main() {
vmName := uuid.New().String()

client, err := client.New()
if err != nil {
log.Fatalf("failed to initialize Orchard API client: %v", err)
}

// Create VM
err = client.VMs().Create(context.Background(), &v1.VM{
Meta: v1.Meta{
Name: vmName,
},
Image: "ghcr.io/cirruslabs/macos-ventura-base:latest",
CPU: 4,
Memory: 4096,
StartupScript: &v1.VMScript{
ScriptContent: "date",
},
})
if err != nil {
log.Fatalf("failed to create VM: %v")
}

// Retrieve VM's logs
for {
vmLogs, err := client.VMs().Logs(context.Background(), vmName)
if err != nil {
log.Fatalf("failed to retrieve VM logs")
}

if len(vmLogs) != 0 {
fmt.Println(vmLogs[0])
break
}

time.Sleep(time.Second)
}

// Delete VM
if err := client.VMs().Delete(context.Background(), vmName); err != nil {
log.Fatalf("failed to delete VM: %v", err)
}
}
```

## Resource management

Some resources, such as `Worker` and `VM`, have a `resource` field which is a dictionary that maps between resource names and their amounts (amount requested or amount provided, depending on the resource) and is useful for scheduling.
Expand Down
22 changes: 22 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ info:
description: Orchard orchestration API
version: 0.1.0
paths:
/controller/info:
get:
summary: "Retrieve controller's information"
tags:
- controller
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#components/schemas/ControllerInfo'
/service-accounts:
post:
summary: "Create a Service Account"
Expand Down Expand Up @@ -321,3 +333,13 @@ components:
type: array
items:
type: string
ControllerInfo:
title: Controller's Information
type: object
properties:
version:
type: string
description: Version number
commit:
type: string
description: Commit hash
9 changes: 7 additions & 2 deletions internal/controller/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ func (controller *Controller) initAPI() *gin.Engine {
// Auth
v1.Use(controller.authenticateMiddleware)

// A way to for the clients to check that the API is working
// OpenAPI docs/spec (if enabled) and a way to for the clients
// to check that the API is working
v1.GET("/", func(c *gin.Context) {
if controller.enableSwaggerDocs {
middleware.SwaggerUI(middleware.SwaggerUIOpts{
Expand All @@ -41,13 +42,17 @@ func (controller *Controller) initAPI() *gin.Engine {
c.Status(http.StatusOK)
}
})

if controller.enableSwaggerDocs {
v1.GET("/openapi.yaml", func(c *gin.Context) {
c.Data(200, "text/yaml", api.Spec)
})
}

// Controller information
v1.GET("/controller/info", func(c *gin.Context) {
controller.controllerInfo(c).Respond(c)
})

// Service accounts
v1.POST("/service-accounts", func(c *gin.Context) {
controller.createServiceAccount(c).Respond(c)
Expand Down
22 changes: 22 additions & 0 deletions internal/controller/api_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package controller

import (
"github.com/cirruslabs/orchard/internal/responder"
"github.com/cirruslabs/orchard/internal/version"
v1pkg "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/gin-gonic/gin"
"net/http"
)

func (controller *Controller) controllerInfo(ctx *gin.Context) responder.Responder {
// Only require the service account to be valid,
// no roles are needed to query this endpoint
if responder := controller.authorize(ctx); responder != nil {
return responder
}

return responder.JSON(http.StatusOK, &v1pkg.ControllerInfo{
Version: version.Version,
Commit: version.Commit,
})
}
1 change: 1 addition & 0 deletions internal/controller/api_vms.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func (controller *Controller) deleteVM(ctx *gin.Context) responder.Responder {
return responder.Code(http.StatusOK)
})
}

func (controller *Controller) appendVMEvents(ctx *gin.Context) responder.Responder {
if responder := controller.authorize(ctx, v1.ServiceAccountRoleComputeWrite); responder != nil {
return responder
Expand Down
6 changes: 6 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,9 @@ func (client *Client) ServiceAccounts() *ServiceAccountsService {
client: client,
}
}

func (client *Client) Controller() *ControllerService {
return &ControllerService{
client: client,
}
}
23 changes: 23 additions & 0 deletions pkg/client/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package client

import (
"context"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"net/http"
)

type ControllerService struct {
client *Client
}

func (service *ControllerService) Info(ctx context.Context) (v1.ControllerInfo, error) {
var controllerInfo v1.ControllerInfo

err := service.client.request(ctx, http.MethodGet, "controller/info", nil, &controllerInfo,
nil)
if err != nil {
return controllerInfo, err
}

return controllerInfo, nil
}
17 changes: 12 additions & 5 deletions pkg/client/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func NewEventStreamer(client *Client, endpoint string) *EventStreamer {
streamer := &EventStreamer{
client: client,
endpoint: endpoint,
eventsChannel: make(chan v1.Event),
eventsChannel: make(chan v1.Event, 64),
fkorotkov marked this conversation as resolved.
Show resolved Hide resolved
}
go streamer.stream()
return streamer
Expand All @@ -46,16 +46,23 @@ func (streamer *EventStreamer) stream() {
}

func (streamer *EventStreamer) readAvailableEvents() ([]v1.Event, bool) {
var result []v1.Event

// blocking wait for at least one event
result := []v1.Event{<-streamer.eventsChannel}
nextEvent, ok := <-streamer.eventsChannel
if !ok {
return result, true
}
result = append(result, nextEvent)

// non-blocking wait for more events, if any
for {
select {
case nextEvent, more := <-streamer.eventsChannel:
result = append(result, nextEvent)
if !more {
case nextEvent, ok := <-streamer.eventsChannel:
if !ok {
return result, true
}
result = append(result, nextEvent)
default:
return result, false
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/resource/v1/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,8 @@ const (
// (either via API or from within a VM via `sudo shutdown -now`).
VMStatusStopped VMStatus = "stopped"
)

type ControllerInfo struct {
Version string `json:"version"`
Commit string `json:"commit"`
}