Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
6 changes: 6 additions & 0 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 {
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"`
EnvKeys *string `json:"env_keys,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 Down
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"
)
93 changes: 87 additions & 6 deletions go/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,34 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
)

// ErrorDetail represents a field-level validation error detail.
type ErrorDetail struct {
Field string `json:"field"`
Value string `json:"value"`
Message string `json:"message"`
}

// ErrorLink represents a reference link attached to an API error.
type ErrorLink struct {
URL string `json:"url"`
Label string `json:"label"`
}

// APIError represents an error response from the Phala Cloud API.
type APIError struct {
StatusCode int
Message string
Detail any
Body string
Headers http.Header
ErrorCode string
StatusCode int
Message string
Detail any
Body string
Headers http.Header
ErrorCode string
Details []ErrorDetail
Suggestions []string
Links []ErrorLink
}

func (e *APIError) Error() string {
Expand Down Expand Up @@ -45,12 +62,22 @@ func (e *APIError) IsServer() bool {
}

// IsRetryable returns true if the error is retryable (409/429/503).
// A 409 Conflict with a structured ErrorCode is a deterministic business error
// and is not retryable.
func (e *APIError) IsRetryable() bool {
if e.StatusCode == http.StatusConflict && e.ErrorCode != "" {
return false
}
return e.StatusCode == http.StatusConflict ||
e.StatusCode == http.StatusTooManyRequests ||
e.StatusCode == http.StatusServiceUnavailable
}

// IsConflict returns true if the error is a 409 Conflict.
func (e *APIError) IsConflict() bool {
return e.StatusCode == http.StatusConflict
}

// IsComposePrecondition returns true if the error is a compose hash precondition failure (465).
func (e *APIError) IsComposePrecondition() bool {
return e.StatusCode == 465
Expand Down Expand Up @@ -79,3 +106,57 @@ func (e *APIError) RetryAfter() time.Duration {
}
return 0
}

// IsStructured returns true when the error includes a structured ErrorCode.
func (e *APIError) IsStructured() bool {
return e.ErrorCode != ""
}

// HasErrorCode returns true if the error has the given error code.
func (e *APIError) HasErrorCode(code string) bool {
return e.ErrorCode == code
}

// FormatError returns a human-readable formatted representation of the error,
// including details, suggestions, and links when available.
func (e *APIError) FormatError() string {
var b strings.Builder
if e.ErrorCode != "" {
fmt.Fprintf(&b, "[%s] ", e.ErrorCode)
}
b.WriteString(e.Message)

if len(e.Details) > 0 {
b.WriteString("\n\nDetails:")
for _, d := range e.Details {
fmt.Fprintf(&b, "\n - %s", d.Message)
if d.Field != "" {
fmt.Fprintf(&b, " (field: %s", d.Field)
if d.Value != "" {
fmt.Fprintf(&b, ", value: %s", d.Value)
}
b.WriteString(")")
}
}
}

if len(e.Suggestions) > 0 {
b.WriteString("\n\nSuggestions:")
for _, s := range e.Suggestions {
fmt.Fprintf(&b, "\n - %s", s)
}
}

if len(e.Links) > 0 {
b.WriteString("\n\nReferences:")
for _, l := range e.Links {
if l.Label != "" {
fmt.Fprintf(&b, "\n - %s: %s", l.Label, l.URL)
} else {
fmt.Fprintf(&b, "\n - %s", l.URL)
}
}
}

return b.String()
}
39 changes: 39 additions & 0 deletions go/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,45 @@ func (c *Client) do(ctx context.Context, req *http.Request) (*http.Response, err
if code, ok := parsed["error_code"].(string); ok {
apiErr.ErrorCode = code
}
// Parse structured error fields.
if details, ok := parsed["details"].([]any); ok {
for _, d := range details {
if dm, ok := d.(map[string]any); ok {
ed := ErrorDetail{}
if f, ok := dm["field"].(string); ok {
ed.Field = f
}
if v, ok := dm["value"].(string); ok {
ed.Value = v
}
if m, ok := dm["message"].(string); ok {
ed.Message = m
}
apiErr.Details = append(apiErr.Details, ed)
}
}
}
if suggestions, ok := parsed["suggestions"].([]any); ok {
for _, s := range suggestions {
if str, ok := s.(string); ok {
apiErr.Suggestions = append(apiErr.Suggestions, str)
}
}
}
if links, ok := parsed["links"].([]any); ok {
for _, l := range links {
if lm, ok := l.(map[string]any); ok {
el := ErrorLink{}
if u, ok := lm["url"].(string); ok {
el.URL = u
Comment thread
Leechael marked this conversation as resolved.
}
if lb, ok := lm["label"].(string); ok {
el.Label = lb
}
apiErr.Links = append(apiErr.Links, el)
}
}
}
}

if apiErr.Message == "" {
Expand Down
24 changes: 12 additions & 12 deletions go/types_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ type CurrentUser struct {

// UserInfo contains user profile information.
type UserInfo struct {
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
Avatar string `json:"avatar"`
EmailVerified bool `json:"email_verified"`
TOTPEnabled bool `json:"totp_enabled"`
HasBackupCodes bool `json:"has_backup_codes"`
FlagHasPassword bool `json:"flag_has_password"`
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
Avatar *string `json:"avatar,omitempty"`
EmailVerified *bool `json:"email_verified,omitempty"`
TOTPEnabled *bool `json:"totp_enabled,omitempty"`
HasBackupCodes *bool `json:"has_backup_codes,omitempty"`
FlagHasPassword *bool `json:"flag_has_password,omitempty"`
}

// WorkspaceInfo contains workspace information for the current user.
Expand All @@ -31,8 +31,8 @@ type WorkspaceInfo struct {

// CreditsInfo contains credit balance information.
type CreditsInfo struct {
Balance any `json:"balance"`
GrantedBalance any `json:"granted_balance"`
IsPostPaid bool `json:"is_post_paid"`
OutstandingAmount any `json:"outstanding_amount,omitempty"`
Balance *string `json:"balance,omitempty"`
GrantedBalance *string `json:"granted_balance,omitempty"`
IsPostPaid *bool `json:"is_post_paid,omitempty"`
OutstandingAmount *string `json:"outstanding_amount,omitempty"`
Comment thread
Leechael marked this conversation as resolved.
}
48 changes: 36 additions & 12 deletions go/types_cvms.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ type CVMInfo struct {
Runner *string `json:"runner,omitempty"`
ManifestVer *string `json:"manifest_version,omitempty"`
ComposeFile any `json:"compose_file,omitempty"`

// Additional fields used by the Terraform provider.
InProgress bool `json:"in_progress,omitempty"`
EncryptedEnvPubkey *string `json:"encrypted_env_pubkey,omitempty"`
DiskSize *int64 `json:"disk_size,omitempty"`
Endpoints []CVMEndpoint `json:"endpoints,omitempty"`
PublicURLs []CVMEndpoint `json:"public_urls,omitempty"`
}

// CVMEndpoint represents a CVM endpoint URL.
type CVMEndpoint struct {
App string `json:"app"`
}

// CvmResource holds CVM resource allocation.
Expand Down Expand Up @@ -134,15 +146,20 @@ type ProvisionCVMRequest struct {
ComposeFile *ComposeFile `json:"compose_file,omitempty"`

// Optional fields.
VCPU *int `json:"vcpu,omitempty"`
Memory *int `json:"memory,omitempty"`
DiskSize *int `json:"disk_size,omitempty"`
TeepodID *int `json:"teepod_id,omitempty"`
Image *string `json:"image,omitempty"`
KMSType *string `json:"kms_type,omitempty"`
Listed *bool `json:"listed,omitempty"`
Encrypted *bool `json:"encrypted,omitempty"`
SecureTime *bool `json:"secure_time,omitempty"`
VCPU *int `json:"vcpu,omitempty"`
Memory *int `json:"memory,omitempty"`
DiskSize *int `json:"disk_size,omitempty"`
TeepodID *int `json:"teepod_id,omitempty"`
Image *string `json:"image,omitempty"`
Region *string `json:"region,omitempty"`
KMSType *string `json:"kms_type,omitempty"`
Listed *bool `json:"listed,omitempty"`
Encrypted *bool `json:"encrypted,omitempty"`
SecureTime *bool `json:"secure_time,omitempty"`
SSHAuthorizedKeys []string `json:"ssh_authorized_keys,omitempty"`
CustomAppID *string `json:"custom_app_id,omitempty"`
Nonce *int64 `json:"nonce,omitempty"`
StorageFS *string `json:"storage_fs,omitempty"`
}

// ComposeFile represents a compose file configuration.
Expand All @@ -153,6 +170,11 @@ type ComposeFile struct {
PreLaunchScript *string `json:"pre_launch_script,omitempty"`
EncryptedEnv *string `json:"encrypted_env,omitempty"`
EnvKeys *string `json:"env_keys,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"`
StorageFS *string `json:"storage_fs,omitempty"`
}

// ProvisionCVMResponse is the response from provisioning a CVM.
Comment thread
Leechael marked this conversation as resolved.
Expand All @@ -171,9 +193,11 @@ type ProvisionCVMResponse struct {

// CommitCVMProvisionRequest is the request for committing a CVM provision.
type CommitCVMProvisionRequest struct {
AppID string `json:"app_id"`
ComposeHash string `json:"compose_hash"`
TransactionHash *string `json:"transaction_hash,omitempty"`
AppID string `json:"app_id"`
ComposeHash string `json:"compose_hash"`
TransactionHash *string `json:"transaction_hash,omitempty"`
EncryptedEnv *string `json:"encrypted_env,omitempty"`
EnvKeys []string `json:"env_keys,omitempty"`
}

// CommitCVMProvisionResponse is the response from committing a CVM provision.
Expand Down
Loading
Loading