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
12 changes: 12 additions & 0 deletions go/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ func (c *Client) GetAppCVMs(ctx context.Context, appID string) ([]GenericObject,
return result, nil
}

// ReplicateAppCVM creates a replica of a CVM within an application context.
// This uses the app-scoped endpoint POST /apps/{appID}/cvms/{vmUUID}/replicas
// to ensure the new replica is associated with the correct app.
func (c *Client) ReplicateAppCVM(ctx context.Context, appID, vmUUID string, opts *ReplicateCVMOptions) (*CVMActionResponse, error) {
var result CVMActionResponse
path := "/apps/" + appID + "/cvms/" + vmUUID + "/replicas"
if err := c.doJSON(ctx, "POST", path, opts, &result); err != nil {
return nil, err
}
return &result, nil
}

// GetAppRevisions returns revisions for an application.
func (c *Client) GetAppRevisions(ctx context.Context, appID string, opts *PaginationOptions) (*AppRevisionsResponse, error) {
path := "/apps/" + appID + "/revisions"
Expand Down
24 changes: 15 additions & 9 deletions go/cvms_compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,17 @@ func (c *Client) GetCVMPreLaunchScript(ctx context.Context, cvmID string) (strin

// ProvisionComposeUpdateRequest is the request for provisioning a compose file update.
type ProvisionComposeUpdateRequest struct {
DockerComposeFile string `json:"docker_compose_file"`
GatewayEnabled *bool `json:"gateway_enabled,omitempty"`
PreLaunchScript *string `json:"pre_launch_script,omitempty"`
EncryptedEnv *string `json:"encrypted_env,omitempty"`
EnvKeys *string `json:"env_keys,omitempty"`
Name string `json:"name"`
DockerComposeFile string `json:"docker_compose_file"`
GatewayEnabled *bool `json:"gateway_enabled,omitempty"`
PreLaunchScript *string `json:"pre_launch_script,omitempty"`
EncryptedEnv *string `json:"encrypted_env,omitempty"`
AllowedEnvs []string `json:"allowed_envs,omitempty"`
PublicLogs *bool `json:"public_logs,omitempty"`
PublicSysinfo *bool `json:"public_sysinfo,omitempty"`
PublicTcbinfo *bool `json:"public_tcbinfo,omitempty"`
SecureTime *bool `json:"secure_time,omitempty"`
UpdateEnvVars *bool `json:"update_env_vars,omitempty"`
}

// ProvisionCVMComposeFileUpdate provisions a compose file update.
Expand All @@ -48,10 +54,10 @@ func (c *Client) ProvisionCVMComposeFileUpdate(ctx context.Context, cvmID string

// CommitComposeUpdateRequest is the request for committing a compose file update.
type CommitComposeUpdateRequest struct {
ComposeHash string `json:"compose_hash"`
EncryptedEnv *string `json:"encrypted_env,omitempty"`
EnvKeys *string `json:"env_keys,omitempty"`
UpdateEnvVars *bool `json:"update_env_vars,omitempty"`
ComposeHash string `json:"compose_hash"`
EncryptedEnv *string `json:"encrypted_env,omitempty"`
EnvKeys []string `json:"env_keys,omitempty"`
UpdateEnvVars *bool `json:"update_env_vars,omitempty"`
}

// CommitCVMComposeFileUpdate commits a compose file update.
Expand Down
8 changes: 4 additions & 4 deletions go/cvms_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ func (c *Client) RefreshCVMInstanceIDs(ctx context.Context, req *RefreshInstance

// UpdateEnvsRequest is the request for updating CVM environment variables.
type UpdateEnvsRequest struct {
EncryptedEnv string `json:"encrypted_env"`
EnvKeys *string `json:"env_keys,omitempty"`
ComposeHash *string `json:"compose_hash,omitempty"`
TransactionHash *string `json:"transaction_hash,omitempty"`
EncryptedEnv string `json:"encrypted_env"`
EnvKeys []string `json:"env_keys,omitempty"`
ComposeHash *string `json:"compose_hash,omitempty"`
TransactionHash *string `json:"transaction_hash,omitempty"`
}

// UpdateCVMEnvs updates the encrypted environment variables for a CVM.
Expand Down
50 changes: 50 additions & 0 deletions go/dstack_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package phala

import dstacksdk "github.com/Dstack-TEE/dstack/sdk/go/dstack"

// EnvVar represents an environment variable key-value pair.
type EnvVar = dstacksdk.EnvVar

// AppCompose represents the application composition structure used for compose hashing.
type AppCompose = dstacksdk.AppCompose

// DockerConfig represents Docker registry credentials used by AppCompose.
type DockerConfig = dstacksdk.DockerConfig

// KeyProviderKind represents the key provider type used by AppCompose.
type KeyProviderKind = dstacksdk.KeyProviderKind

const (
KeyProviderNone = dstacksdk.KeyProviderNone
KeyProviderKMS = dstacksdk.KeyProviderKMS
KeyProviderLocal = dstacksdk.KeyProviderLocal
)

// VerifyEnvEncryptPublicKeyOptions configures timestamp validation for signature verification.
type VerifyEnvEncryptPublicKeyOptions = dstacksdk.VerifyEnvEncryptPublicKeyOptions

// EncryptEnvVars encrypts environment variables using the upstream dstack Go SDK implementation.
func EncryptEnvVars(envs []EnvVar, publicKeyHex string) (string, error) {
return dstacksdk.EncryptEnvVars(envs, publicKeyHex)
}

// GetComposeHash computes the compose hash using the upstream dstack Go SDK implementation.
func GetComposeHash(appCompose AppCompose, normalize ...bool) (string, error) {
return dstacksdk.GetComposeHash(appCompose, normalize...)
}

// VerifyEnvEncryptPublicKey verifies the signature of an env encryption public key.
func VerifyEnvEncryptPublicKey(publicKey []byte, signature []byte, appID string) ([]byte, error) {
return dstacksdk.VerifyEnvEncryptPublicKey(publicKey, signature, appID)
}

// VerifyEnvEncryptPublicKeyWithTimestamp verifies a timestamped env encryption public-key signature.
func VerifyEnvEncryptPublicKeyWithTimestamp(
publicKey []byte,
signature []byte,
appID string,
timestamp uint64,
opts *VerifyEnvEncryptPublicKeyOptions,
) ([]byte, error) {
return dstacksdk.VerifyEnvEncryptPublicKeyWithTimestamp(publicKey, signature, appID, timestamp, opts)
}
94 changes: 94 additions & 0 deletions go/dstack_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package phala

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"encoding/json"
"testing"

"golang.org/x/crypto/curve25519"
)

func TestEncryptEnvVars(t *testing.T) {
remotePriv := make([]byte, 32)
if _, err := rand.Read(remotePriv); err != nil {
t.Fatal(err)
}
remotePub, err := curve25519.X25519(remotePriv, curve25519.Basepoint)
if err != nil {
t.Fatal(err)
}

envs := []EnvVar{
{Key: "NODE_ENV", Value: "production"},
{Key: "MESSAGE", Value: "Hello world"},
}

encryptedHex, err := EncryptEnvVars(envs, hex.EncodeToString(remotePub))
if err != nil {
t.Fatal(err)
}

encrypted, err := hex.DecodeString(encryptedHex)
if err != nil {
t.Fatal(err)
}
if len(encrypted) <= 44 {
t.Fatalf("expected encrypted payload > 44 bytes, got %d", len(encrypted))
}

ephemeralPub := encrypted[:32]
iv := encrypted[32:44]
ciphertext := encrypted[44:]

sharedSecret, err := curve25519.X25519(remotePriv, ephemeralPub)
if err != nil {
t.Fatal(err)
}

block, err := aes.NewCipher(sharedSecret)
if err != nil {
t.Fatal(err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
t.Fatal(err)
}

plaintext, err := gcm.Open(nil, iv, ciphertext, nil)
if err != nil {
t.Fatal(err)
}

var payload struct {
Env []EnvVar `json:"env"`
}
if err := json.Unmarshal(plaintext, &payload); err != nil {
t.Fatal(err)
}

if len(payload.Env) != len(envs) {
t.Fatalf("expected %d env vars, got %d", len(envs), len(payload.Env))
}
for i := range envs {
if payload.Env[i] != envs[i] {
t.Fatalf("env var mismatch at %d: expected %+v, got %+v", i, envs[i], payload.Env[i])
}
}
}

func TestGetComposeHash(t *testing.T) {
got, err := GetComposeHash(AppCompose{
Runner: "docker-compose",
DockerComposeFile: "services:\n app:\n image: nginx:latest\n",
AllowedEnvs: []string{"API_KEY"},
}, true)
if err != nil {
t.Fatal(err)
}
if len(got) != 64 {
t.Fatalf("expected 64-char sha256 hex, got %q", got)
}
}
67 changes: 67 additions & 0 deletions go/error_codes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package phala

// Structured error codes returned by the Phala Cloud API.
// Use with APIError.HasErrorCode() to match specific errors.
//
// Error codes follow the format ERR-{MODULE}-{CODE} where MODULE is a
// two-digit module identifier and CODE is a three-digit sequential number.

// Module 01: CVM Preflight & Compose Hash
const (
ErrNodeNotFound = "ERR-01-001"
ErrComposeFileRequired = "ERR-01-002"
ErrInvalidComposeFile = "ERR-01-003"
ErrDuplicateCvmName = "ERR-01-004"
ErrHashRegistration = "ERR-01-005"
ErrHashInvalidExpired = "ERR-01-006"
ErrTxVerifyFailed = "ERR-01-007"
ErrHashNotAllowed = "ERR-01-008"
)

// Module 02: Inventory
const (
ErrInstanceTypeNotFound = "ERR-02-001"
ErrResourceNotAvailable = "ERR-02-002"
ErrInsufficientVcpu = "ERR-02-003"
ErrInsufficientMemory = "ERR-02-004"
ErrInsufficientSlots = "ERR-02-005"
ErrGpuAllocation = "ERR-02-006"
ErrInsufficientGpu = "ERR-02-007"
ErrInvalidRequest = "ERR-02-008"
ErrIncompatibleConfig = "ERR-02-009"
ErrImageNotFound = "ERR-02-010"
ErrKmsNotFound = "ERR-02-011"
ErrTeepodNotAccessible = "ERR-02-012"
ErrOsImageNotCompatible = "ERR-02-013"
ErrNodeCapacityNotConfig = "ERR-02-014"
ErrQuotaExceeded = "ERR-02-015"
)

// Module 03: CVM Operations
const (
ErrCvmNotFound = "ERR-03-001"
ErrMultipleCvmsSameName = "ERR-03-002"
// ERR-03-003 and ERR-03-004 are CvmNotInWorkspaceError variants (reveal/hide existence).
ErrCvmNotInWorkspace = "ERR-03-003"
ErrCvmAccessDenied = "ERR-03-005"
ErrReplicaImageNotAvail = "ERR-03-006"
ErrCvmAppIdConflict = "ERR-03-007"
)

// Module 04: Workspace
const (
ErrInsufficientBalance = "ERR-04-001"
ErrMaxCvmLimit = "ERR-04-002"
ErrResourceLimitExceed = "ERR-04-003"
)

// Module 05: Credentials
const (
ErrTokenLimitExceeded = "ERR-05-001"
ErrTokenRateLimit = "ERR-05-002"
)

// Module 06: Auth
const (
ErrOAuthEmailInvalid = "ERR-06-001"
)
Loading
Loading