Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
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
12 changes: 8 additions & 4 deletions go/types_ssh_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package phala

// SSHKey represents an SSH key.
type SSHKey struct {
ID string `json:"id"`
Name string `json:"name"`
PublicKey string `json:"public_key"`
CreatedAt *string `json:"created_at,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
PublicKey string `json:"public_key"`
Fingerprint *string `json:"fingerprint,omitempty"`
KeyType *string `json:"key_type,omitempty"`
Source *string `json:"source,omitempty"`
CreatedAt *string `json:"created_at,omitempty"`
UpdatedAt *string `json:"updated_at,omitempty"`
}

// CreateSSHKeyRequest is the request for creating an SSH key.
Expand Down
Loading
Loading