Skip to content
Closed
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ under `machines ssh` and `machines executions`.

The generated contract is defined by the [OpenAPI snapshot](https://storage.googleapis.com/stainless-sdk-openapi-specs/dedalus-labs/dedalus-c0e28b234478af75f61bcf42f5f32818d96ec93b1d8239fa0ff1c801c0e4b64f.yml).

### Selecting the create API

`dedalus machines create` uses `/v1/machines` by default. To request controller
placement from a server that supports `/v2/machines`, select v2 explicitly:

```sh
dedalus machines create --api-version v2 --vcpu 2 --memory-mib 4096 --storage-gib 20
```

The request body and authentication are unchanged. V2 returns acceptance before
the Machine is running. Use the existing retrieve and lifecycle commands for the
returned Machine. An unavailable v2 endpoint returns an error without retrying v1.

### Running commands on a machine

`exec` is an alias for `executions`. Pass the executable and its arguments after
Expand Down
85 changes: 85 additions & 0 deletions pkg/cmd/create_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// @custom start
// Copyright (c) 2026 Dedalus Labs, Inc. All rights reserved.

package cmd

import (
"encoding/json"
"net/http"
"net/http/httptest"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// A create selects exactly one API version, preserving its body and credentials.
// Invalid versions send nothing. An unavailable v2 never triggers a v1 create.
func TestCreateAPIVersion(t *testing.T) {
binary := filepath.Join(t.TempDir(), "dedalus")
if runtime.GOOS == "windows" {
binary += ".exe"
}
output, err := exec.Command("go", "build", "-o", binary, "../../cmd/dedalus").CombinedOutput()
require.NoError(t, err, "%s", output)
for _, test := range []struct {
name, version, prefix, stdin, path, errorText string
status, requests int
}{
{name: "default", path: "/v1/machines", status: 202, requests: 1},
{name: "explicit v1", version: "v1", path: "/v1/machines", status: 202, requests: 1},
{name: "v2", version: "v2", path: "/v2/machines", status: 202, requests: 1},
{name: "v2 gateway prefix", version: "v2", prefix: "/dcs", path: "/dcs/v2/machines", status: 202, requests: 1},
{name: "v2 piped body", version: "v2", stdin: `{"vcpu":2,"memory_mib":2048,"storage_gib":20,"autosleep":"never"}`, path: "/v2/machines", status: 202, requests: 1},
{name: "invalid", version: "v3", errorText: "api-version must be v1 or v2"},
{name: "v2 unavailable", version: "v2", path: "/v2/machines", status: 404, requests: 1, errorText: "404"},
} {
t.Run(test.name, func(t *testing.T) {
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, test.path, r.URL.Path)
assert.Equal(t, "Bearer test-key", r.Header.Get("Authorization"))
var body map[string]any
assert.NoError(t, json.NewDecoder(r.Body).Decode(&body))
assert.Equal(t, map[string]any{"vcpu": float64(2), "memory_mib": float64(2048), "storage_gib": float64(20), "autosleep": "never"}, body)
w.Header().Set("Content-Type", "application/json")
if test.status == 0 {
t.Error("invalid version reached the API")
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(test.status)
_, err := w.Write([]byte(`{"machine_id":"dm-test","phase":"pending"}`))
assert.NoError(t, err)
}))
defer server.Close()
args := []string{"--base-url", server.URL + test.prefix, "--api-key", "test-key", "machines", "create"}
if test.version != "" {
args = append(args, "--api-version", test.version)
}
if test.stdin == "" {
args = append(args, "--vcpu", "2", "--memory-mib", "2048", "--storage-gib", "20", "--autosleep", "never")
}
command := exec.Command(binary, args...)
command.Stdin = strings.NewReader(test.stdin)
output, err := command.CombinedOutput()
if test.errorText != "" {
require.Error(t, err, "%s", output)
require.Contains(t, string(output), test.errorText)
} else {
require.NoError(t, err, "%s", output)
require.Contains(t, string(output), "dm-test")
}
require.Equal(t, int32(test.requests), calls.Load())
})
}
}

// @custom end
19 changes: 17 additions & 2 deletions pkg/cmd/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ var machinesCreate = cli.Command{
Usage: "Create machine",
Suggest: true,
Flags: []cli.Flag{
// @custom start
&cli.StringFlag{
Name: "api-version",
Usage: "Create endpoint version: v1 or v2 (controller placement)",
Value: "v1",
Validator: func(version string) error {
if version != "v1" && version != "v2" {
return fmt.Errorf("api-version must be v1 or v2")
}
return nil
},
},
// @custom end
&requestflag.Flag[string]{
Name: "autosleep",
Usage: `Idle window before autosleep. Accepts fixed duration units like 30s, 30m, 2h, 7d3h4s, or 1w3d, raw seconds ("1800"), or never to disable.`,
Expand Down Expand Up @@ -187,8 +200,10 @@ func handleMachinesCreate(ctx context.Context, cmd *cli.Command) error {
params := dedalus.MachineNewParams{}

var res []byte
options = append(options, option.WithResponseBodyInto(&res))
_, err = client.Machines.New(ctx, params, options...)
// @custom start
// Use the selected endpoint with the existing body, transport, and errors.
err = client.Post(ctx, cmd.String("api-version")+"/machines", params, &res, options...)
// @custom end
if err != nil {
return err
}
Expand Down
Loading