diff --git a/README.md b/README.md index 98fde5e..242fa41 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ strategies, and adding new operations. | `node.status.get` | Get node status | Read-only | [docs](docs/orchestration/node-status.md) | | `node.disk.get` | Get disk usage | Read-only | [docs](docs/orchestration/node-disk.md) | | `node.memory.get` | Get memory stats | Read-only | [docs](docs/orchestration/node-memory.md) | +| `node.uptime.get` | Get system uptime | Read-only | [docs](docs/orchestration/node-uptime.md) | | `node.load.get` | Get load averages | Read-only | [docs](docs/orchestration/node-load.md) | | `network.dns.get` | Get DNS configuration | Read-only | [docs](docs/orchestration/network-dns-get.md) | | `network.dns.update` | Update DNS servers | Yes | [docs](docs/orchestration/network-dns-update.md) | diff --git a/docs/gen/gen.md b/docs/gen/gen.md index 5d3d689..aa7d121 100644 --- a/docs/gen/gen.md +++ b/docs/gen/gen.md @@ -255,6 +255,8 @@ Package gen contains generated code for the OSAPI REST API client. - [type MemoryResponse](<#MemoryResponse>) - [type MemoryResultItem](<#MemoryResultItem>) - [type NATSInfo](<#NATSInfo>) +- [type NetworkInterfaceResponse](<#NetworkInterfaceResponse>) +- [type NetworkInterfaceResponseFamily](<#NetworkInterfaceResponseFamily>) - [type NodeStatusCollectionResponse](<#NodeStatusCollectionResponse>) - [type NodeStatusResponse](<#NodeStatusResponse>) - [type OSInfoCollectionResponse](<#OSInfoCollectionResponse>) @@ -314,7 +316,7 @@ const ( ``` -## func [NewDeleteJobByIDRequest]() +## func [NewDeleteJobByIDRequest]() ```go func NewDeleteJobByIDRequest(server string, id openapi_types.UUID) (*http.Request, error) @@ -323,7 +325,7 @@ func NewDeleteJobByIDRequest(server string, id openapi_types.UUID) (*http.Reques NewDeleteJobByIDRequest generates requests for DeleteJobByID -## func [NewGetAgentDetailsRequest]() +## func [NewGetAgentDetailsRequest]() ```go func NewGetAgentDetailsRequest(server string, hostname string) (*http.Request, error) @@ -332,7 +334,7 @@ func NewGetAgentDetailsRequest(server string, hostname string) (*http.Request, e NewGetAgentDetailsRequest generates requests for GetAgentDetails -## func [NewGetAgentRequest]() +## func [NewGetAgentRequest]() ```go func NewGetAgentRequest(server string) (*http.Request, error) @@ -341,7 +343,7 @@ func NewGetAgentRequest(server string) (*http.Request, error) NewGetAgentRequest generates requests for GetAgent -## func [NewGetAuditExportRequest]() +## func [NewGetAuditExportRequest]() ```go func NewGetAuditExportRequest(server string) (*http.Request, error) @@ -350,7 +352,7 @@ func NewGetAuditExportRequest(server string) (*http.Request, error) NewGetAuditExportRequest generates requests for GetAuditExport -## func [NewGetAuditLogByIDRequest]() +## func [NewGetAuditLogByIDRequest]() ```go func NewGetAuditLogByIDRequest(server string, id openapi_types.UUID) (*http.Request, error) @@ -359,7 +361,7 @@ func NewGetAuditLogByIDRequest(server string, id openapi_types.UUID) (*http.Requ NewGetAuditLogByIDRequest generates requests for GetAuditLogByID -## func [NewGetAuditLogsRequest]() +## func [NewGetAuditLogsRequest]() ```go func NewGetAuditLogsRequest(server string, params *GetAuditLogsParams) (*http.Request, error) @@ -368,7 +370,7 @@ func NewGetAuditLogsRequest(server string, params *GetAuditLogsParams) (*http.Re NewGetAuditLogsRequest generates requests for GetAuditLogs -## func [NewGetHealthReadyRequest]() +## func [NewGetHealthReadyRequest]() ```go func NewGetHealthReadyRequest(server string) (*http.Request, error) @@ -377,7 +379,7 @@ func NewGetHealthReadyRequest(server string) (*http.Request, error) NewGetHealthReadyRequest generates requests for GetHealthReady -## func [NewGetHealthRequest]() +## func [NewGetHealthRequest]() ```go func NewGetHealthRequest(server string) (*http.Request, error) @@ -386,7 +388,7 @@ func NewGetHealthRequest(server string) (*http.Request, error) NewGetHealthRequest generates requests for GetHealth -## func [NewGetHealthStatusRequest]() +## func [NewGetHealthStatusRequest]() ```go func NewGetHealthStatusRequest(server string) (*http.Request, error) @@ -395,7 +397,7 @@ func NewGetHealthStatusRequest(server string) (*http.Request, error) NewGetHealthStatusRequest generates requests for GetHealthStatus -## func [NewGetJobByIDRequest]() +## func [NewGetJobByIDRequest]() ```go func NewGetJobByIDRequest(server string, id openapi_types.UUID) (*http.Request, error) @@ -404,7 +406,7 @@ func NewGetJobByIDRequest(server string, id openapi_types.UUID) (*http.Request, NewGetJobByIDRequest generates requests for GetJobByID -## func [NewGetJobRequest]() +## func [NewGetJobRequest]() ```go func NewGetJobRequest(server string, params *GetJobParams) (*http.Request, error) @@ -413,7 +415,7 @@ func NewGetJobRequest(server string, params *GetJobParams) (*http.Request, error NewGetJobRequest generates requests for GetJob -## func [NewGetJobStatusRequest]() +## func [NewGetJobStatusRequest]() ```go func NewGetJobStatusRequest(server string) (*http.Request, error) @@ -422,7 +424,7 @@ func NewGetJobStatusRequest(server string) (*http.Request, error) NewGetJobStatusRequest generates requests for GetJobStatus -## func [NewGetNodeDiskRequest]() +## func [NewGetNodeDiskRequest]() ```go func NewGetNodeDiskRequest(server string, hostname Hostname) (*http.Request, error) @@ -431,7 +433,7 @@ func NewGetNodeDiskRequest(server string, hostname Hostname) (*http.Request, err NewGetNodeDiskRequest generates requests for GetNodeDisk -## func [NewGetNodeHostnameRequest]() +## func [NewGetNodeHostnameRequest]() ```go func NewGetNodeHostnameRequest(server string, hostname Hostname) (*http.Request, error) @@ -440,7 +442,7 @@ func NewGetNodeHostnameRequest(server string, hostname Hostname) (*http.Request, NewGetNodeHostnameRequest generates requests for GetNodeHostname -## func [NewGetNodeLoadRequest]() +## func [NewGetNodeLoadRequest]() ```go func NewGetNodeLoadRequest(server string, hostname Hostname) (*http.Request, error) @@ -449,7 +451,7 @@ func NewGetNodeLoadRequest(server string, hostname Hostname) (*http.Request, err NewGetNodeLoadRequest generates requests for GetNodeLoad -## func [NewGetNodeMemoryRequest]() +## func [NewGetNodeMemoryRequest]() ```go func NewGetNodeMemoryRequest(server string, hostname Hostname) (*http.Request, error) @@ -458,7 +460,7 @@ func NewGetNodeMemoryRequest(server string, hostname Hostname) (*http.Request, e NewGetNodeMemoryRequest generates requests for GetNodeMemory -## func [NewGetNodeNetworkDNSByInterfaceRequest]() +## func [NewGetNodeNetworkDNSByInterfaceRequest]() ```go func NewGetNodeNetworkDNSByInterfaceRequest(server string, hostname Hostname, interfaceName string) (*http.Request, error) @@ -467,7 +469,7 @@ func NewGetNodeNetworkDNSByInterfaceRequest(server string, hostname Hostname, in NewGetNodeNetworkDNSByInterfaceRequest generates requests for GetNodeNetworkDNSByInterface -## func [NewGetNodeOSRequest]() +## func [NewGetNodeOSRequest]() ```go func NewGetNodeOSRequest(server string, hostname Hostname) (*http.Request, error) @@ -476,7 +478,7 @@ func NewGetNodeOSRequest(server string, hostname Hostname) (*http.Request, error NewGetNodeOSRequest generates requests for GetNodeOS -## func [NewGetNodeStatusRequest]() +## func [NewGetNodeStatusRequest]() ```go func NewGetNodeStatusRequest(server string, hostname Hostname) (*http.Request, error) @@ -485,7 +487,7 @@ func NewGetNodeStatusRequest(server string, hostname Hostname) (*http.Request, e NewGetNodeStatusRequest generates requests for GetNodeStatus -## func [NewGetNodeUptimeRequest]() +## func [NewGetNodeUptimeRequest]() ```go func NewGetNodeUptimeRequest(server string, hostname Hostname) (*http.Request, error) @@ -494,7 +496,7 @@ func NewGetNodeUptimeRequest(server string, hostname Hostname) (*http.Request, e NewGetNodeUptimeRequest generates requests for GetNodeUptime -## func [NewGetVersionRequest]() +## func [NewGetVersionRequest]() ```go func NewGetVersionRequest(server string) (*http.Request, error) @@ -503,7 +505,7 @@ func NewGetVersionRequest(server string) (*http.Request, error) NewGetVersionRequest generates requests for GetVersion -## func [NewPostJobRequest]() +## func [NewPostJobRequest]() ```go func NewPostJobRequest(server string, body PostJobJSONRequestBody) (*http.Request, error) @@ -512,7 +514,7 @@ func NewPostJobRequest(server string, body PostJobJSONRequestBody) (*http.Reques NewPostJobRequest calls the generic PostJob builder with application/json body -## func [NewPostJobRequestWithBody]() +## func [NewPostJobRequestWithBody]() ```go func NewPostJobRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) @@ -521,7 +523,7 @@ func NewPostJobRequestWithBody(server string, contentType string, body io.Reader NewPostJobRequestWithBody generates requests for PostJob with any type of body -## func [NewPostNodeCommandExecRequest]() +## func [NewPostNodeCommandExecRequest]() ```go func NewPostNodeCommandExecRequest(server string, hostname Hostname, body PostNodeCommandExecJSONRequestBody) (*http.Request, error) @@ -530,7 +532,7 @@ func NewPostNodeCommandExecRequest(server string, hostname Hostname, body PostNo NewPostNodeCommandExecRequest calls the generic PostNodeCommandExec builder with application/json body -## func [NewPostNodeCommandExecRequestWithBody]() +## func [NewPostNodeCommandExecRequestWithBody]() ```go func NewPostNodeCommandExecRequestWithBody(server string, hostname Hostname, contentType string, body io.Reader) (*http.Request, error) @@ -539,7 +541,7 @@ func NewPostNodeCommandExecRequestWithBody(server string, hostname Hostname, con NewPostNodeCommandExecRequestWithBody generates requests for PostNodeCommandExec with any type of body -## func [NewPostNodeCommandShellRequest]() +## func [NewPostNodeCommandShellRequest]() ```go func NewPostNodeCommandShellRequest(server string, hostname Hostname, body PostNodeCommandShellJSONRequestBody) (*http.Request, error) @@ -548,7 +550,7 @@ func NewPostNodeCommandShellRequest(server string, hostname Hostname, body PostN NewPostNodeCommandShellRequest calls the generic PostNodeCommandShell builder with application/json body -## func [NewPostNodeCommandShellRequestWithBody]() +## func [NewPostNodeCommandShellRequestWithBody]() ```go func NewPostNodeCommandShellRequestWithBody(server string, hostname Hostname, contentType string, body io.Reader) (*http.Request, error) @@ -557,7 +559,7 @@ func NewPostNodeCommandShellRequestWithBody(server string, hostname Hostname, co NewPostNodeCommandShellRequestWithBody generates requests for PostNodeCommandShell with any type of body -## func [NewPostNodeNetworkPingRequest]() +## func [NewPostNodeNetworkPingRequest]() ```go func NewPostNodeNetworkPingRequest(server string, hostname Hostname, body PostNodeNetworkPingJSONRequestBody) (*http.Request, error) @@ -566,7 +568,7 @@ func NewPostNodeNetworkPingRequest(server string, hostname Hostname, body PostNo NewPostNodeNetworkPingRequest calls the generic PostNodeNetworkPing builder with application/json body -## func [NewPostNodeNetworkPingRequestWithBody]() +## func [NewPostNodeNetworkPingRequestWithBody]() ```go func NewPostNodeNetworkPingRequestWithBody(server string, hostname Hostname, contentType string, body io.Reader) (*http.Request, error) @@ -575,7 +577,7 @@ func NewPostNodeNetworkPingRequestWithBody(server string, hostname Hostname, con NewPostNodeNetworkPingRequestWithBody generates requests for PostNodeNetworkPing with any type of body -## func [NewPutNodeNetworkDNSRequest]() +## func [NewPutNodeNetworkDNSRequest]() ```go func NewPutNodeNetworkDNSRequest(server string, hostname Hostname, body PutNodeNetworkDNSJSONRequestBody) (*http.Request, error) @@ -584,7 +586,7 @@ func NewPutNodeNetworkDNSRequest(server string, hostname Hostname, body PutNodeN NewPutNodeNetworkDNSRequest calls the generic PutNodeNetworkDNS builder with application/json body -## func [NewPutNodeNetworkDNSRequestWithBody]() +## func [NewPutNodeNetworkDNSRequestWithBody]() ```go func NewPutNodeNetworkDNSRequestWithBody(server string, hostname Hostname, contentType string, body io.Reader) (*http.Request, error) @@ -593,7 +595,7 @@ func NewPutNodeNetworkDNSRequestWithBody(server string, hostname Hostname, conte NewPutNodeNetworkDNSRequestWithBody generates requests for PutNodeNetworkDNS with any type of body -## func [NewRetryJobByIDRequest]() +## func [NewRetryJobByIDRequest]() ```go func NewRetryJobByIDRequest(server string, id openapi_types.UUID, body RetryJobByIDJSONRequestBody) (*http.Request, error) @@ -602,7 +604,7 @@ func NewRetryJobByIDRequest(server string, id openapi_types.UUID, body RetryJobB NewRetryJobByIDRequest calls the generic RetryJobByID builder with application/json body -## func [NewRetryJobByIDRequestWithBody]() +## func [NewRetryJobByIDRequestWithBody]() ```go func NewRetryJobByIDRequestWithBody(server string, id openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) @@ -611,7 +613,7 @@ func NewRetryJobByIDRequestWithBody(server string, id openapi_types.UUID, conten NewRetryJobByIDRequestWithBody generates requests for RetryJobByID with any type of body -## type [AgentDetail]() +## type [AgentDetail]() AgentDetail defines model for AgentDetail. @@ -629,14 +631,30 @@ type AgentDetail struct { ``` -## type [AgentInfo]() +## type [AgentInfo]() AgentInfo defines model for AgentInfo. ```go type AgentInfo struct { + // Architecture CPU architecture. + Architecture *string `json:"architecture,omitempty"` + + // CpuCount Number of logical CPUs. + CpuCount *int `json:"cpu_count,omitempty"` + + // Facts Extended facts from additional providers. + Facts *map[string]interface{} `json:"facts,omitempty"` + + // Fqdn Fully qualified domain name. + Fqdn *string `json:"fqdn,omitempty"` + // Hostname The hostname of the agent. - Hostname string `json:"hostname"` + Hostname string `json:"hostname"` + Interfaces *[]NetworkInterfaceResponse `json:"interfaces,omitempty"` + + // KernelVersion OS kernel version. + KernelVersion *string `json:"kernel_version,omitempty"` // Labels Key-value labels configured on the agent. Labels *map[string]string `json:"labels,omitempty"` @@ -650,9 +668,15 @@ type AgentInfo struct { // OsInfo Operating system information. OsInfo *OSInfoResponse `json:"os_info,omitempty"` + // PackageMgr Package manager. + PackageMgr *string `json:"package_mgr,omitempty"` + // RegisteredAt When the agent last refreshed its heartbeat. RegisteredAt *time.Time `json:"registered_at,omitempty"` + // ServiceMgr Init system. + ServiceMgr *string `json:"service_mgr,omitempty"` + // StartedAt When the agent process started. StartedAt *time.Time `json:"started_at,omitempty"` @@ -665,7 +689,7 @@ type AgentInfo struct { ``` -## type [AgentInfoStatus]() +## type [AgentInfoStatus]() AgentInfoStatus The current status of the agent. @@ -683,7 +707,7 @@ const ( ``` -## type [AgentStats]() +## type [AgentStats]() AgentStats defines model for AgentStats. @@ -701,7 +725,7 @@ type AgentStats struct { ``` -## type [AuditEntry]() +## type [AuditEntry]() AuditEntry defines model for AuditEntry. @@ -740,7 +764,7 @@ type AuditEntry struct { ``` -## type [AuditEntryResponse]() +## type [AuditEntryResponse]() AuditEntryResponse defines model for AuditEntryResponse. @@ -751,7 +775,7 @@ type AuditEntryResponse struct { ``` -## type [Client]() +## type [Client]() Client which conforms to the OpenAPI3 specification for this service. @@ -774,7 +798,7 @@ type Client struct { ``` -### func [NewClient]() +### func [NewClient]() ```go func NewClient(server string, opts ...ClientOption) (*Client, error) @@ -783,7 +807,7 @@ func NewClient(server string, opts ...ClientOption) (*Client, error) Creates a new Client, with reasonable defaults -### func \(\*Client\) [DeleteJobByID]() +### func \(\*Client\) [DeleteJobByID]() ```go func (c *Client) DeleteJobByID(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -792,7 +816,7 @@ func (c *Client) DeleteJobByID(ctx context.Context, id openapi_types.UUID, reqEd -### func \(\*Client\) [GetAgent]() +### func \(\*Client\) [GetAgent]() ```go func (c *Client) GetAgent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -801,7 +825,7 @@ func (c *Client) GetAgent(ctx context.Context, reqEditors ...RequestEditorFn) (* -### func \(\*Client\) [GetAgentDetails]() +### func \(\*Client\) [GetAgentDetails]() ```go func (c *Client) GetAgentDetails(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -810,7 +834,7 @@ func (c *Client) GetAgentDetails(ctx context.Context, hostname string, reqEditor -### func \(\*Client\) [GetAuditExport]() +### func \(\*Client\) [GetAuditExport]() ```go func (c *Client) GetAuditExport(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -819,7 +843,7 @@ func (c *Client) GetAuditExport(ctx context.Context, reqEditors ...RequestEditor -### func \(\*Client\) [GetAuditLogByID]() +### func \(\*Client\) [GetAuditLogByID]() ```go func (c *Client) GetAuditLogByID(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -828,7 +852,7 @@ func (c *Client) GetAuditLogByID(ctx context.Context, id openapi_types.UUID, req -### func \(\*Client\) [GetAuditLogs]() +### func \(\*Client\) [GetAuditLogs]() ```go func (c *Client) GetAuditLogs(ctx context.Context, params *GetAuditLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -837,7 +861,7 @@ func (c *Client) GetAuditLogs(ctx context.Context, params *GetAuditLogsParams, r -### func \(\*Client\) [GetHealth]() +### func \(\*Client\) [GetHealth]() ```go func (c *Client) GetHealth(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -846,7 +870,7 @@ func (c *Client) GetHealth(ctx context.Context, reqEditors ...RequestEditorFn) ( -### func \(\*Client\) [GetHealthReady]() +### func \(\*Client\) [GetHealthReady]() ```go func (c *Client) GetHealthReady(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -855,7 +879,7 @@ func (c *Client) GetHealthReady(ctx context.Context, reqEditors ...RequestEditor -### func \(\*Client\) [GetHealthStatus]() +### func \(\*Client\) [GetHealthStatus]() ```go func (c *Client) GetHealthStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -864,7 +888,7 @@ func (c *Client) GetHealthStatus(ctx context.Context, reqEditors ...RequestEdito -### func \(\*Client\) [GetJob]() +### func \(\*Client\) [GetJob]() ```go func (c *Client) GetJob(ctx context.Context, params *GetJobParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -873,7 +897,7 @@ func (c *Client) GetJob(ctx context.Context, params *GetJobParams, reqEditors .. -### func \(\*Client\) [GetJobByID]() +### func \(\*Client\) [GetJobByID]() ```go func (c *Client) GetJobByID(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -882,7 +906,7 @@ func (c *Client) GetJobByID(ctx context.Context, id openapi_types.UUID, reqEdito -### func \(\*Client\) [GetJobStatus]() +### func \(\*Client\) [GetJobStatus]() ```go func (c *Client) GetJobStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -891,7 +915,7 @@ func (c *Client) GetJobStatus(ctx context.Context, reqEditors ...RequestEditorFn -### func \(\*Client\) [GetNodeDisk]() +### func \(\*Client\) [GetNodeDisk]() ```go func (c *Client) GetNodeDisk(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -900,7 +924,7 @@ func (c *Client) GetNodeDisk(ctx context.Context, hostname Hostname, reqEditors -### func \(\*Client\) [GetNodeHostname]() +### func \(\*Client\) [GetNodeHostname]() ```go func (c *Client) GetNodeHostname(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -909,7 +933,7 @@ func (c *Client) GetNodeHostname(ctx context.Context, hostname Hostname, reqEdit -### func \(\*Client\) [GetNodeLoad]() +### func \(\*Client\) [GetNodeLoad]() ```go func (c *Client) GetNodeLoad(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -918,7 +942,7 @@ func (c *Client) GetNodeLoad(ctx context.Context, hostname Hostname, reqEditors -### func \(\*Client\) [GetNodeMemory]() +### func \(\*Client\) [GetNodeMemory]() ```go func (c *Client) GetNodeMemory(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -927,7 +951,7 @@ func (c *Client) GetNodeMemory(ctx context.Context, hostname Hostname, reqEditor -### func \(\*Client\) [GetNodeNetworkDNSByInterface]() +### func \(\*Client\) [GetNodeNetworkDNSByInterface]() ```go func (c *Client) GetNodeNetworkDNSByInterface(ctx context.Context, hostname Hostname, interfaceName string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -936,7 +960,7 @@ func (c *Client) GetNodeNetworkDNSByInterface(ctx context.Context, hostname Host -### func \(\*Client\) [GetNodeOS]() +### func \(\*Client\) [GetNodeOS]() ```go func (c *Client) GetNodeOS(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -945,7 +969,7 @@ func (c *Client) GetNodeOS(ctx context.Context, hostname Hostname, reqEditors .. -### func \(\*Client\) [GetNodeStatus]() +### func \(\*Client\) [GetNodeStatus]() ```go func (c *Client) GetNodeStatus(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -954,7 +978,7 @@ func (c *Client) GetNodeStatus(ctx context.Context, hostname Hostname, reqEditor -### func \(\*Client\) [GetNodeUptime]() +### func \(\*Client\) [GetNodeUptime]() ```go func (c *Client) GetNodeUptime(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -963,7 +987,7 @@ func (c *Client) GetNodeUptime(ctx context.Context, hostname Hostname, reqEditor -### func \(\*Client\) [GetVersion]() +### func \(\*Client\) [GetVersion]() ```go func (c *Client) GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -972,7 +996,7 @@ func (c *Client) GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) -### func \(\*Client\) [PostJob]() +### func \(\*Client\) [PostJob]() ```go func (c *Client) PostJob(ctx context.Context, body PostJobJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -981,7 +1005,7 @@ func (c *Client) PostJob(ctx context.Context, body PostJobJSONRequestBody, reqEd -### func \(\*Client\) [PostJobWithBody]() +### func \(\*Client\) [PostJobWithBody]() ```go func (c *Client) PostJobWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -990,7 +1014,7 @@ func (c *Client) PostJobWithBody(ctx context.Context, contentType string, body i -### func \(\*Client\) [PostNodeCommandExec]() +### func \(\*Client\) [PostNodeCommandExec]() ```go func (c *Client) PostNodeCommandExec(ctx context.Context, hostname Hostname, body PostNodeCommandExecJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -999,7 +1023,7 @@ func (c *Client) PostNodeCommandExec(ctx context.Context, hostname Hostname, bod -### func \(\*Client\) [PostNodeCommandExecWithBody]() +### func \(\*Client\) [PostNodeCommandExecWithBody]() ```go func (c *Client) PostNodeCommandExecWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1008,7 +1032,7 @@ func (c *Client) PostNodeCommandExecWithBody(ctx context.Context, hostname Hostn -### func \(\*Client\) [PostNodeCommandShell]() +### func \(\*Client\) [PostNodeCommandShell]() ```go func (c *Client) PostNodeCommandShell(ctx context.Context, hostname Hostname, body PostNodeCommandShellJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1017,7 +1041,7 @@ func (c *Client) PostNodeCommandShell(ctx context.Context, hostname Hostname, bo -### func \(\*Client\) [PostNodeCommandShellWithBody]() +### func \(\*Client\) [PostNodeCommandShellWithBody]() ```go func (c *Client) PostNodeCommandShellWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1026,7 +1050,7 @@ func (c *Client) PostNodeCommandShellWithBody(ctx context.Context, hostname Host -### func \(\*Client\) [PostNodeNetworkPing]() +### func \(\*Client\) [PostNodeNetworkPing]() ```go func (c *Client) PostNodeNetworkPing(ctx context.Context, hostname Hostname, body PostNodeNetworkPingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1035,7 +1059,7 @@ func (c *Client) PostNodeNetworkPing(ctx context.Context, hostname Hostname, bod -### func \(\*Client\) [PostNodeNetworkPingWithBody]() +### func \(\*Client\) [PostNodeNetworkPingWithBody]() ```go func (c *Client) PostNodeNetworkPingWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1044,7 +1068,7 @@ func (c *Client) PostNodeNetworkPingWithBody(ctx context.Context, hostname Hostn -### func \(\*Client\) [PutNodeNetworkDNS]() +### func \(\*Client\) [PutNodeNetworkDNS]() ```go func (c *Client) PutNodeNetworkDNS(ctx context.Context, hostname Hostname, body PutNodeNetworkDNSJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1053,7 +1077,7 @@ func (c *Client) PutNodeNetworkDNS(ctx context.Context, hostname Hostname, body -### func \(\*Client\) [PutNodeNetworkDNSWithBody]() +### func \(\*Client\) [PutNodeNetworkDNSWithBody]() ```go func (c *Client) PutNodeNetworkDNSWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1062,7 +1086,7 @@ func (c *Client) PutNodeNetworkDNSWithBody(ctx context.Context, hostname Hostnam -### func \(\*Client\) [RetryJobByID]() +### func \(\*Client\) [RetryJobByID]() ```go func (c *Client) RetryJobByID(ctx context.Context, id openapi_types.UUID, body RetryJobByIDJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1071,7 +1095,7 @@ func (c *Client) RetryJobByID(ctx context.Context, id openapi_types.UUID, body R -### func \(\*Client\) [RetryJobByIDWithBody]() +### func \(\*Client\) [RetryJobByIDWithBody]() ```go func (c *Client) RetryJobByIDWithBody(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1080,7 +1104,7 @@ func (c *Client) RetryJobByIDWithBody(ctx context.Context, id openapi_types.UUID -## type [ClientInterface]() +## type [ClientInterface]() The interface specification for the client above. @@ -1182,7 +1206,7 @@ type ClientInterface interface { ``` -## type [ClientOption]() +## type [ClientOption]() ClientOption allows setting custom parameters during construction @@ -1191,7 +1215,7 @@ type ClientOption func(*Client) error ``` -### func [WithBaseURL]() +### func [WithBaseURL]() ```go func WithBaseURL(baseURL string) ClientOption @@ -1200,7 +1224,7 @@ func WithBaseURL(baseURL string) ClientOption WithBaseURL overrides the baseURL. -### func [WithHTTPClient]() +### func [WithHTTPClient]() ```go func WithHTTPClient(doer HttpRequestDoer) ClientOption @@ -1209,7 +1233,7 @@ func WithHTTPClient(doer HttpRequestDoer) ClientOption WithHTTPClient allows overriding the default Doer, which is automatically created using http.Client. This is useful for tests. -### func [WithRequestEditorFn]() +### func [WithRequestEditorFn]() ```go func WithRequestEditorFn(fn RequestEditorFn) ClientOption @@ -1218,7 +1242,7 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption WithRequestEditorFn allows setting up a callback function, which will be called right before sending the request. This can be used to mutate the request. -## type [ClientWithResponses]() +## type [ClientWithResponses]() ClientWithResponses builds on ClientInterface to offer response payloads @@ -1229,7 +1253,7 @@ type ClientWithResponses struct { ``` -### func [NewClientWithResponses]() +### func [NewClientWithResponses]() ```go func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) @@ -1238,7 +1262,7 @@ func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithRes NewClientWithResponses creates a new ClientWithResponses, which wraps Client with return type handling -### func \(\*ClientWithResponses\) [DeleteJobByIDWithResponse]() +### func \(\*ClientWithResponses\) [DeleteJobByIDWithResponse]() ```go func (c *ClientWithResponses) DeleteJobByIDWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteJobByIDResponse, error) @@ -1247,7 +1271,7 @@ func (c *ClientWithResponses) DeleteJobByIDWithResponse(ctx context.Context, id DeleteJobByIDWithResponse request returning \*DeleteJobByIDResponse -### func \(\*ClientWithResponses\) [GetAgentDetailsWithResponse]() +### func \(\*ClientWithResponses\) [GetAgentDetailsWithResponse]() ```go func (c *ClientWithResponses) GetAgentDetailsWithResponse(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*GetAgentDetailsResponse, error) @@ -1256,7 +1280,7 @@ func (c *ClientWithResponses) GetAgentDetailsWithResponse(ctx context.Context, h GetAgentDetailsWithResponse request returning \*GetAgentDetailsResponse -### func \(\*ClientWithResponses\) [GetAgentWithResponse]() +### func \(\*ClientWithResponses\) [GetAgentWithResponse]() ```go func (c *ClientWithResponses) GetAgentWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetAgentResponse, error) @@ -1265,7 +1289,7 @@ func (c *ClientWithResponses) GetAgentWithResponse(ctx context.Context, reqEdito GetAgentWithResponse request returning \*GetAgentResponse -### func \(\*ClientWithResponses\) [GetAuditExportWithResponse]() +### func \(\*ClientWithResponses\) [GetAuditExportWithResponse]() ```go func (c *ClientWithResponses) GetAuditExportWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetAuditExportResponse, error) @@ -1274,7 +1298,7 @@ func (c *ClientWithResponses) GetAuditExportWithResponse(ctx context.Context, re GetAuditExportWithResponse request returning \*GetAuditExportResponse -### func \(\*ClientWithResponses\) [GetAuditLogByIDWithResponse]() +### func \(\*ClientWithResponses\) [GetAuditLogByIDWithResponse]() ```go func (c *ClientWithResponses) GetAuditLogByIDWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetAuditLogByIDResponse, error) @@ -1283,7 +1307,7 @@ func (c *ClientWithResponses) GetAuditLogByIDWithResponse(ctx context.Context, i GetAuditLogByIDWithResponse request returning \*GetAuditLogByIDResponse -### func \(\*ClientWithResponses\) [GetAuditLogsWithResponse]() +### func \(\*ClientWithResponses\) [GetAuditLogsWithResponse]() ```go func (c *ClientWithResponses) GetAuditLogsWithResponse(ctx context.Context, params *GetAuditLogsParams, reqEditors ...RequestEditorFn) (*GetAuditLogsResponse, error) @@ -1292,7 +1316,7 @@ func (c *ClientWithResponses) GetAuditLogsWithResponse(ctx context.Context, para GetAuditLogsWithResponse request returning \*GetAuditLogsResponse -### func \(\*ClientWithResponses\) [GetHealthReadyWithResponse]() +### func \(\*ClientWithResponses\) [GetHealthReadyWithResponse]() ```go func (c *ClientWithResponses) GetHealthReadyWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetHealthReadyResponse, error) @@ -1301,7 +1325,7 @@ func (c *ClientWithResponses) GetHealthReadyWithResponse(ctx context.Context, re GetHealthReadyWithResponse request returning \*GetHealthReadyResponse -### func \(\*ClientWithResponses\) [GetHealthStatusWithResponse]() +### func \(\*ClientWithResponses\) [GetHealthStatusWithResponse]() ```go func (c *ClientWithResponses) GetHealthStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetHealthStatusResponse, error) @@ -1310,7 +1334,7 @@ func (c *ClientWithResponses) GetHealthStatusWithResponse(ctx context.Context, r GetHealthStatusWithResponse request returning \*GetHealthStatusResponse -### func \(\*ClientWithResponses\) [GetHealthWithResponse]() +### func \(\*ClientWithResponses\) [GetHealthWithResponse]() ```go func (c *ClientWithResponses) GetHealthWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetHealthResponse, error) @@ -1319,7 +1343,7 @@ func (c *ClientWithResponses) GetHealthWithResponse(ctx context.Context, reqEdit GetHealthWithResponse request returning \*GetHealthResponse -### func \(\*ClientWithResponses\) [GetJobByIDWithResponse]() +### func \(\*ClientWithResponses\) [GetJobByIDWithResponse]() ```go func (c *ClientWithResponses) GetJobByIDWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetJobByIDResponse, error) @@ -1328,7 +1352,7 @@ func (c *ClientWithResponses) GetJobByIDWithResponse(ctx context.Context, id ope GetJobByIDWithResponse request returning \*GetJobByIDResponse -### func \(\*ClientWithResponses\) [GetJobStatusWithResponse]() +### func \(\*ClientWithResponses\) [GetJobStatusWithResponse]() ```go func (c *ClientWithResponses) GetJobStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetJobStatusResponse, error) @@ -1337,7 +1361,7 @@ func (c *ClientWithResponses) GetJobStatusWithResponse(ctx context.Context, reqE GetJobStatusWithResponse request returning \*GetJobStatusResponse -### func \(\*ClientWithResponses\) [GetJobWithResponse]() +### func \(\*ClientWithResponses\) [GetJobWithResponse]() ```go func (c *ClientWithResponses) GetJobWithResponse(ctx context.Context, params *GetJobParams, reqEditors ...RequestEditorFn) (*GetJobResponse, error) @@ -1346,7 +1370,7 @@ func (c *ClientWithResponses) GetJobWithResponse(ctx context.Context, params *Ge GetJobWithResponse request returning \*GetJobResponse -### func \(\*ClientWithResponses\) [GetNodeDiskWithResponse]() +### func \(\*ClientWithResponses\) [GetNodeDiskWithResponse]() ```go func (c *ClientWithResponses) GetNodeDiskWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeDiskResponse, error) @@ -1355,7 +1379,7 @@ func (c *ClientWithResponses) GetNodeDiskWithResponse(ctx context.Context, hostn GetNodeDiskWithResponse request returning \*GetNodeDiskResponse -### func \(\*ClientWithResponses\) [GetNodeHostnameWithResponse]() +### func \(\*ClientWithResponses\) [GetNodeHostnameWithResponse]() ```go func (c *ClientWithResponses) GetNodeHostnameWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeHostnameResponse, error) @@ -1364,7 +1388,7 @@ func (c *ClientWithResponses) GetNodeHostnameWithResponse(ctx context.Context, h GetNodeHostnameWithResponse request returning \*GetNodeHostnameResponse -### func \(\*ClientWithResponses\) [GetNodeLoadWithResponse]() +### func \(\*ClientWithResponses\) [GetNodeLoadWithResponse]() ```go func (c *ClientWithResponses) GetNodeLoadWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeLoadResponse, error) @@ -1373,7 +1397,7 @@ func (c *ClientWithResponses) GetNodeLoadWithResponse(ctx context.Context, hostn GetNodeLoadWithResponse request returning \*GetNodeLoadResponse -### func \(\*ClientWithResponses\) [GetNodeMemoryWithResponse]() +### func \(\*ClientWithResponses\) [GetNodeMemoryWithResponse]() ```go func (c *ClientWithResponses) GetNodeMemoryWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeMemoryResponse, error) @@ -1382,7 +1406,7 @@ func (c *ClientWithResponses) GetNodeMemoryWithResponse(ctx context.Context, hos GetNodeMemoryWithResponse request returning \*GetNodeMemoryResponse -### func \(\*ClientWithResponses\) [GetNodeNetworkDNSByInterfaceWithResponse]() +### func \(\*ClientWithResponses\) [GetNodeNetworkDNSByInterfaceWithResponse]() ```go func (c *ClientWithResponses) GetNodeNetworkDNSByInterfaceWithResponse(ctx context.Context, hostname Hostname, interfaceName string, reqEditors ...RequestEditorFn) (*GetNodeNetworkDNSByInterfaceResponse, error) @@ -1391,7 +1415,7 @@ func (c *ClientWithResponses) GetNodeNetworkDNSByInterfaceWithResponse(ctx conte GetNodeNetworkDNSByInterfaceWithResponse request returning \*GetNodeNetworkDNSByInterfaceResponse -### func \(\*ClientWithResponses\) [GetNodeOSWithResponse]() +### func \(\*ClientWithResponses\) [GetNodeOSWithResponse]() ```go func (c *ClientWithResponses) GetNodeOSWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeOSResponse, error) @@ -1400,7 +1424,7 @@ func (c *ClientWithResponses) GetNodeOSWithResponse(ctx context.Context, hostnam GetNodeOSWithResponse request returning \*GetNodeOSResponse -### func \(\*ClientWithResponses\) [GetNodeStatusWithResponse]() +### func \(\*ClientWithResponses\) [GetNodeStatusWithResponse]() ```go func (c *ClientWithResponses) GetNodeStatusWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeStatusResponse, error) @@ -1409,7 +1433,7 @@ func (c *ClientWithResponses) GetNodeStatusWithResponse(ctx context.Context, hos GetNodeStatusWithResponse request returning \*GetNodeStatusResponse -### func \(\*ClientWithResponses\) [GetNodeUptimeWithResponse]() +### func \(\*ClientWithResponses\) [GetNodeUptimeWithResponse]() ```go func (c *ClientWithResponses) GetNodeUptimeWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeUptimeResponse, error) @@ -1418,7 +1442,7 @@ func (c *ClientWithResponses) GetNodeUptimeWithResponse(ctx context.Context, hos GetNodeUptimeWithResponse request returning \*GetNodeUptimeResponse -### func \(\*ClientWithResponses\) [GetVersionWithResponse]() +### func \(\*ClientWithResponses\) [GetVersionWithResponse]() ```go func (c *ClientWithResponses) GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error) @@ -1427,7 +1451,7 @@ func (c *ClientWithResponses) GetVersionWithResponse(ctx context.Context, reqEdi GetVersionWithResponse request returning \*GetVersionResponse -### func \(\*ClientWithResponses\) [PostJobWithBodyWithResponse]() +### func \(\*ClientWithResponses\) [PostJobWithBodyWithResponse]() ```go func (c *ClientWithResponses) PostJobWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostJobResponse, error) @@ -1436,7 +1460,7 @@ func (c *ClientWithResponses) PostJobWithBodyWithResponse(ctx context.Context, c PostJobWithBodyWithResponse request with arbitrary body returning \*PostJobResponse -### func \(\*ClientWithResponses\) [PostJobWithResponse]() +### func \(\*ClientWithResponses\) [PostJobWithResponse]() ```go func (c *ClientWithResponses) PostJobWithResponse(ctx context.Context, body PostJobJSONRequestBody, reqEditors ...RequestEditorFn) (*PostJobResponse, error) @@ -1445,7 +1469,7 @@ func (c *ClientWithResponses) PostJobWithResponse(ctx context.Context, body Post -### func \(\*ClientWithResponses\) [PostNodeCommandExecWithBodyWithResponse]() +### func \(\*ClientWithResponses\) [PostNodeCommandExecWithBodyWithResponse]() ```go func (c *ClientWithResponses) PostNodeCommandExecWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeCommandExecResponse, error) @@ -1454,7 +1478,7 @@ func (c *ClientWithResponses) PostNodeCommandExecWithBodyWithResponse(ctx contex PostNodeCommandExecWithBodyWithResponse request with arbitrary body returning \*PostNodeCommandExecResponse -### func \(\*ClientWithResponses\) [PostNodeCommandExecWithResponse]() +### func \(\*ClientWithResponses\) [PostNodeCommandExecWithResponse]() ```go func (c *ClientWithResponses) PostNodeCommandExecWithResponse(ctx context.Context, hostname Hostname, body PostNodeCommandExecJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeCommandExecResponse, error) @@ -1463,7 +1487,7 @@ func (c *ClientWithResponses) PostNodeCommandExecWithResponse(ctx context.Contex -### func \(\*ClientWithResponses\) [PostNodeCommandShellWithBodyWithResponse]() +### func \(\*ClientWithResponses\) [PostNodeCommandShellWithBodyWithResponse]() ```go func (c *ClientWithResponses) PostNodeCommandShellWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeCommandShellResponse, error) @@ -1472,7 +1496,7 @@ func (c *ClientWithResponses) PostNodeCommandShellWithBodyWithResponse(ctx conte PostNodeCommandShellWithBodyWithResponse request with arbitrary body returning \*PostNodeCommandShellResponse -### func \(\*ClientWithResponses\) [PostNodeCommandShellWithResponse]() +### func \(\*ClientWithResponses\) [PostNodeCommandShellWithResponse]() ```go func (c *ClientWithResponses) PostNodeCommandShellWithResponse(ctx context.Context, hostname Hostname, body PostNodeCommandShellJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeCommandShellResponse, error) @@ -1481,7 +1505,7 @@ func (c *ClientWithResponses) PostNodeCommandShellWithResponse(ctx context.Conte -### func \(\*ClientWithResponses\) [PostNodeNetworkPingWithBodyWithResponse]() +### func \(\*ClientWithResponses\) [PostNodeNetworkPingWithBodyWithResponse]() ```go func (c *ClientWithResponses) PostNodeNetworkPingWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeNetworkPingResponse, error) @@ -1490,7 +1514,7 @@ func (c *ClientWithResponses) PostNodeNetworkPingWithBodyWithResponse(ctx contex PostNodeNetworkPingWithBodyWithResponse request with arbitrary body returning \*PostNodeNetworkPingResponse -### func \(\*ClientWithResponses\) [PostNodeNetworkPingWithResponse]() +### func \(\*ClientWithResponses\) [PostNodeNetworkPingWithResponse]() ```go func (c *ClientWithResponses) PostNodeNetworkPingWithResponse(ctx context.Context, hostname Hostname, body PostNodeNetworkPingJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeNetworkPingResponse, error) @@ -1499,7 +1523,7 @@ func (c *ClientWithResponses) PostNodeNetworkPingWithResponse(ctx context.Contex -### func \(\*ClientWithResponses\) [PutNodeNetworkDNSWithBodyWithResponse]() +### func \(\*ClientWithResponses\) [PutNodeNetworkDNSWithBodyWithResponse]() ```go func (c *ClientWithResponses) PutNodeNetworkDNSWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutNodeNetworkDNSResponse, error) @@ -1508,7 +1532,7 @@ func (c *ClientWithResponses) PutNodeNetworkDNSWithBodyWithResponse(ctx context. PutNodeNetworkDNSWithBodyWithResponse request with arbitrary body returning \*PutNodeNetworkDNSResponse -### func \(\*ClientWithResponses\) [PutNodeNetworkDNSWithResponse]() +### func \(\*ClientWithResponses\) [PutNodeNetworkDNSWithResponse]() ```go func (c *ClientWithResponses) PutNodeNetworkDNSWithResponse(ctx context.Context, hostname Hostname, body PutNodeNetworkDNSJSONRequestBody, reqEditors ...RequestEditorFn) (*PutNodeNetworkDNSResponse, error) @@ -1517,7 +1541,7 @@ func (c *ClientWithResponses) PutNodeNetworkDNSWithResponse(ctx context.Context, -### func \(\*ClientWithResponses\) [RetryJobByIDWithBodyWithResponse]() +### func \(\*ClientWithResponses\) [RetryJobByIDWithBodyWithResponse]() ```go func (c *ClientWithResponses) RetryJobByIDWithBodyWithResponse(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RetryJobByIDResponse, error) @@ -1526,7 +1550,7 @@ func (c *ClientWithResponses) RetryJobByIDWithBodyWithResponse(ctx context.Conte RetryJobByIDWithBodyWithResponse request with arbitrary body returning \*RetryJobByIDResponse -### func \(\*ClientWithResponses\) [RetryJobByIDWithResponse]() +### func \(\*ClientWithResponses\) [RetryJobByIDWithResponse]() ```go func (c *ClientWithResponses) RetryJobByIDWithResponse(ctx context.Context, id openapi_types.UUID, body RetryJobByIDJSONRequestBody, reqEditors ...RequestEditorFn) (*RetryJobByIDResponse, error) @@ -1535,7 +1559,7 @@ func (c *ClientWithResponses) RetryJobByIDWithResponse(ctx context.Context, id o -## type [ClientWithResponsesInterface]() +## type [ClientWithResponsesInterface]() ClientWithResponsesInterface is the interface specification for the client with responses above. @@ -1637,7 +1661,7 @@ type ClientWithResponsesInterface interface { ``` -## type [CommandExecRequest]() +## type [CommandExecRequest]() CommandExecRequest defines model for CommandExecRequest. @@ -1658,7 +1682,7 @@ type CommandExecRequest struct { ``` -## type [CommandResultCollectionResponse]() +## type [CommandResultCollectionResponse]() CommandResultCollectionResponse defines model for CommandResultCollectionResponse. @@ -1671,7 +1695,7 @@ type CommandResultCollectionResponse struct { ``` -## type [CommandResultItem]() +## type [CommandResultItem]() CommandResultItem defines model for CommandResultItem. @@ -1701,7 +1725,7 @@ type CommandResultItem struct { ``` -## type [CommandShellRequest]() +## type [CommandShellRequest]() CommandShellRequest defines model for CommandShellRequest. @@ -1719,7 +1743,7 @@ type CommandShellRequest struct { ``` -## type [ComponentHealth]() +## type [ComponentHealth]() ComponentHealth defines model for ComponentHealth. @@ -1734,7 +1758,7 @@ type ComponentHealth struct { ``` -## type [ConsumerDetail]() +## type [ConsumerDetail]() ConsumerDetail defines model for ConsumerDetail. @@ -1755,7 +1779,7 @@ type ConsumerDetail struct { ``` -## type [ConsumerStats]() +## type [ConsumerStats]() ConsumerStats defines model for ConsumerStats. @@ -1770,7 +1794,7 @@ type ConsumerStats struct { ``` -## type [CreateJobRequest]() +## type [CreateJobRequest]() CreateJobRequest defines model for CreateJobRequest. @@ -1785,7 +1809,7 @@ type CreateJobRequest struct { ``` -## type [CreateJobResponse]() +## type [CreateJobResponse]() CreateJobResponse defines model for CreateJobResponse. @@ -1806,7 +1830,7 @@ type CreateJobResponse struct { ``` -## type [DNSConfigCollectionResponse]() +## type [DNSConfigCollectionResponse]() DNSConfigCollectionResponse defines model for DNSConfigCollectionResponse. @@ -1819,7 +1843,7 @@ type DNSConfigCollectionResponse struct { ``` -## type [DNSConfigResponse]() +## type [DNSConfigResponse]() DNSConfigResponse defines model for DNSConfigResponse. @@ -1840,7 +1864,7 @@ type DNSConfigResponse struct { ``` -## type [DNSConfigUpdateRequest]() +## type [DNSConfigUpdateRequest]() DNSConfigUpdateRequest defines model for DNSConfigUpdateRequest. @@ -1858,7 +1882,7 @@ type DNSConfigUpdateRequest struct { ``` -## type [DNSUpdateCollectionResponse]() +## type [DNSUpdateCollectionResponse]() DNSUpdateCollectionResponse defines model for DNSUpdateCollectionResponse. @@ -1871,7 +1895,7 @@ type DNSUpdateCollectionResponse struct { ``` -## type [DNSUpdateResultItem]() +## type [DNSUpdateResultItem]() DNSUpdateResultItem defines model for DNSUpdateResultItem. @@ -1886,7 +1910,7 @@ type DNSUpdateResultItem struct { ``` -## type [DNSUpdateResultItemStatus]() +## type [DNSUpdateResultItemStatus]() DNSUpdateResultItemStatus defines model for DNSUpdateResultItem.Status. @@ -1904,7 +1928,7 @@ const ( ``` -## type [DeleteJobByIDResponse]() +## type [DeleteJobByIDResponse]() @@ -1921,7 +1945,7 @@ type DeleteJobByIDResponse struct { ``` -### func [ParseDeleteJobByIDResponse]() +### func [ParseDeleteJobByIDResponse]() ```go func ParseDeleteJobByIDResponse(rsp *http.Response) (*DeleteJobByIDResponse, error) @@ -1930,7 +1954,7 @@ func ParseDeleteJobByIDResponse(rsp *http.Response) (*DeleteJobByIDResponse, err ParseDeleteJobByIDResponse parses an HTTP response from a DeleteJobByIDWithResponse call -### func \(DeleteJobByIDResponse\) [Status]() +### func \(DeleteJobByIDResponse\) [Status]() ```go func (r DeleteJobByIDResponse) Status() string @@ -1939,7 +1963,7 @@ func (r DeleteJobByIDResponse) Status() string Status returns HTTPResponse.Status -### func \(DeleteJobByIDResponse\) [StatusCode]() +### func \(DeleteJobByIDResponse\) [StatusCode]() ```go func (r DeleteJobByIDResponse) StatusCode() int @@ -1948,7 +1972,7 @@ func (r DeleteJobByIDResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [DiskCollectionResponse]() +## type [DiskCollectionResponse]() DiskCollectionResponse defines model for DiskCollectionResponse. @@ -1961,7 +1985,7 @@ type DiskCollectionResponse struct { ``` -## type [DiskResponse]() +## type [DiskResponse]() DiskResponse Local disk usage information. @@ -1982,7 +2006,7 @@ type DiskResponse struct { ``` -## type [DiskResultItem]() +## type [DiskResultItem]() DiskResultItem defines model for DiskResultItem. @@ -2000,7 +2024,7 @@ type DiskResultItem struct { ``` -## type [DisksResponse]() +## type [DisksResponse]() DisksResponse List of local disk usage information. @@ -2009,7 +2033,7 @@ type DisksResponse = []DiskResponse ``` -## type [ErrorResponse]() +## type [ErrorResponse]() ErrorResponse defines model for ErrorResponse. @@ -2027,7 +2051,7 @@ type ErrorResponse struct { ``` -## type [GetAgentDetailsResponse]() +## type [GetAgentDetailsResponse]() @@ -2044,7 +2068,7 @@ type GetAgentDetailsResponse struct { ``` -### func [ParseGetAgentDetailsResponse]() +### func [ParseGetAgentDetailsResponse]() ```go func ParseGetAgentDetailsResponse(rsp *http.Response) (*GetAgentDetailsResponse, error) @@ -2053,7 +2077,7 @@ func ParseGetAgentDetailsResponse(rsp *http.Response) (*GetAgentDetailsResponse, ParseGetAgentDetailsResponse parses an HTTP response from a GetAgentDetailsWithResponse call -### func \(GetAgentDetailsResponse\) [Status]() +### func \(GetAgentDetailsResponse\) [Status]() ```go func (r GetAgentDetailsResponse) Status() string @@ -2062,7 +2086,7 @@ func (r GetAgentDetailsResponse) Status() string Status returns HTTPResponse.Status -### func \(GetAgentDetailsResponse\) [StatusCode]() +### func \(GetAgentDetailsResponse\) [StatusCode]() ```go func (r GetAgentDetailsResponse) StatusCode() int @@ -2071,7 +2095,7 @@ func (r GetAgentDetailsResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetAgentResponse]() +## type [GetAgentResponse]() @@ -2087,7 +2111,7 @@ type GetAgentResponse struct { ``` -### func [ParseGetAgentResponse]() +### func [ParseGetAgentResponse]() ```go func ParseGetAgentResponse(rsp *http.Response) (*GetAgentResponse, error) @@ -2096,7 +2120,7 @@ func ParseGetAgentResponse(rsp *http.Response) (*GetAgentResponse, error) ParseGetAgentResponse parses an HTTP response from a GetAgentWithResponse call -### func \(GetAgentResponse\) [Status]() +### func \(GetAgentResponse\) [Status]() ```go func (r GetAgentResponse) Status() string @@ -2105,7 +2129,7 @@ func (r GetAgentResponse) Status() string Status returns HTTPResponse.Status -### func \(GetAgentResponse\) [StatusCode]() +### func \(GetAgentResponse\) [StatusCode]() ```go func (r GetAgentResponse) StatusCode() int @@ -2114,7 +2138,7 @@ func (r GetAgentResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetAuditExportResponse]() +## type [GetAuditExportResponse]() @@ -2130,7 +2154,7 @@ type GetAuditExportResponse struct { ``` -### func [ParseGetAuditExportResponse]() +### func [ParseGetAuditExportResponse]() ```go func ParseGetAuditExportResponse(rsp *http.Response) (*GetAuditExportResponse, error) @@ -2139,7 +2163,7 @@ func ParseGetAuditExportResponse(rsp *http.Response) (*GetAuditExportResponse, e ParseGetAuditExportResponse parses an HTTP response from a GetAuditExportWithResponse call -### func \(GetAuditExportResponse\) [Status]() +### func \(GetAuditExportResponse\) [Status]() ```go func (r GetAuditExportResponse) Status() string @@ -2148,7 +2172,7 @@ func (r GetAuditExportResponse) Status() string Status returns HTTPResponse.Status -### func \(GetAuditExportResponse\) [StatusCode]() +### func \(GetAuditExportResponse\) [StatusCode]() ```go func (r GetAuditExportResponse) StatusCode() int @@ -2157,7 +2181,7 @@ func (r GetAuditExportResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetAuditLogByIDResponse]() +## type [GetAuditLogByIDResponse]() @@ -2174,7 +2198,7 @@ type GetAuditLogByIDResponse struct { ``` -### func [ParseGetAuditLogByIDResponse]() +### func [ParseGetAuditLogByIDResponse]() ```go func ParseGetAuditLogByIDResponse(rsp *http.Response) (*GetAuditLogByIDResponse, error) @@ -2183,7 +2207,7 @@ func ParseGetAuditLogByIDResponse(rsp *http.Response) (*GetAuditLogByIDResponse, ParseGetAuditLogByIDResponse parses an HTTP response from a GetAuditLogByIDWithResponse call -### func \(GetAuditLogByIDResponse\) [Status]() +### func \(GetAuditLogByIDResponse\) [Status]() ```go func (r GetAuditLogByIDResponse) Status() string @@ -2192,7 +2216,7 @@ func (r GetAuditLogByIDResponse) Status() string Status returns HTTPResponse.Status -### func \(GetAuditLogByIDResponse\) [StatusCode]() +### func \(GetAuditLogByIDResponse\) [StatusCode]() ```go func (r GetAuditLogByIDResponse) StatusCode() int @@ -2201,7 +2225,7 @@ func (r GetAuditLogByIDResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetAuditLogsParams]() +## type [GetAuditLogsParams]() GetAuditLogsParams defines parameters for GetAuditLogs. @@ -2216,7 +2240,7 @@ type GetAuditLogsParams struct { ``` -## type [GetAuditLogsResponse]() +## type [GetAuditLogsResponse]() @@ -2233,7 +2257,7 @@ type GetAuditLogsResponse struct { ``` -### func [ParseGetAuditLogsResponse]() +### func [ParseGetAuditLogsResponse]() ```go func ParseGetAuditLogsResponse(rsp *http.Response) (*GetAuditLogsResponse, error) @@ -2242,7 +2266,7 @@ func ParseGetAuditLogsResponse(rsp *http.Response) (*GetAuditLogsResponse, error ParseGetAuditLogsResponse parses an HTTP response from a GetAuditLogsWithResponse call -### func \(GetAuditLogsResponse\) [Status]() +### func \(GetAuditLogsResponse\) [Status]() ```go func (r GetAuditLogsResponse) Status() string @@ -2251,7 +2275,7 @@ func (r GetAuditLogsResponse) Status() string Status returns HTTPResponse.Status -### func \(GetAuditLogsResponse\) [StatusCode]() +### func \(GetAuditLogsResponse\) [StatusCode]() ```go func (r GetAuditLogsResponse) StatusCode() int @@ -2260,7 +2284,7 @@ func (r GetAuditLogsResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetHealthReadyResponse]() +## type [GetHealthReadyResponse]() @@ -2274,7 +2298,7 @@ type GetHealthReadyResponse struct { ``` -### func [ParseGetHealthReadyResponse]() +### func [ParseGetHealthReadyResponse]() ```go func ParseGetHealthReadyResponse(rsp *http.Response) (*GetHealthReadyResponse, error) @@ -2283,7 +2307,7 @@ func ParseGetHealthReadyResponse(rsp *http.Response) (*GetHealthReadyResponse, e ParseGetHealthReadyResponse parses an HTTP response from a GetHealthReadyWithResponse call -### func \(GetHealthReadyResponse\) [Status]() +### func \(GetHealthReadyResponse\) [Status]() ```go func (r GetHealthReadyResponse) Status() string @@ -2292,7 +2316,7 @@ func (r GetHealthReadyResponse) Status() string Status returns HTTPResponse.Status -### func \(GetHealthReadyResponse\) [StatusCode]() +### func \(GetHealthReadyResponse\) [StatusCode]() ```go func (r GetHealthReadyResponse) StatusCode() int @@ -2301,7 +2325,7 @@ func (r GetHealthReadyResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetHealthResponse]() +## type [GetHealthResponse]() @@ -2314,7 +2338,7 @@ type GetHealthResponse struct { ``` -### func [ParseGetHealthResponse]() +### func [ParseGetHealthResponse]() ```go func ParseGetHealthResponse(rsp *http.Response) (*GetHealthResponse, error) @@ -2323,7 +2347,7 @@ func ParseGetHealthResponse(rsp *http.Response) (*GetHealthResponse, error) ParseGetHealthResponse parses an HTTP response from a GetHealthWithResponse call -### func \(GetHealthResponse\) [Status]() +### func \(GetHealthResponse\) [Status]() ```go func (r GetHealthResponse) Status() string @@ -2332,7 +2356,7 @@ func (r GetHealthResponse) Status() string Status returns HTTPResponse.Status -### func \(GetHealthResponse\) [StatusCode]() +### func \(GetHealthResponse\) [StatusCode]() ```go func (r GetHealthResponse) StatusCode() int @@ -2341,7 +2365,7 @@ func (r GetHealthResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetHealthStatusResponse]() +## type [GetHealthStatusResponse]() @@ -2357,7 +2381,7 @@ type GetHealthStatusResponse struct { ``` -### func [ParseGetHealthStatusResponse]() +### func [ParseGetHealthStatusResponse]() ```go func ParseGetHealthStatusResponse(rsp *http.Response) (*GetHealthStatusResponse, error) @@ -2366,7 +2390,7 @@ func ParseGetHealthStatusResponse(rsp *http.Response) (*GetHealthStatusResponse, ParseGetHealthStatusResponse parses an HTTP response from a GetHealthStatusWithResponse call -### func \(GetHealthStatusResponse\) [Status]() +### func \(GetHealthStatusResponse\) [Status]() ```go func (r GetHealthStatusResponse) Status() string @@ -2375,7 +2399,7 @@ func (r GetHealthStatusResponse) Status() string Status returns HTTPResponse.Status -### func \(GetHealthStatusResponse\) [StatusCode]() +### func \(GetHealthStatusResponse\) [StatusCode]() ```go func (r GetHealthStatusResponse) StatusCode() int @@ -2384,7 +2408,7 @@ func (r GetHealthStatusResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetJobByIDResponse]() +## type [GetJobByIDResponse]() @@ -2402,7 +2426,7 @@ type GetJobByIDResponse struct { ``` -### func [ParseGetJobByIDResponse]() +### func [ParseGetJobByIDResponse]() ```go func ParseGetJobByIDResponse(rsp *http.Response) (*GetJobByIDResponse, error) @@ -2411,7 +2435,7 @@ func ParseGetJobByIDResponse(rsp *http.Response) (*GetJobByIDResponse, error) ParseGetJobByIDResponse parses an HTTP response from a GetJobByIDWithResponse call -### func \(GetJobByIDResponse\) [Status]() +### func \(GetJobByIDResponse\) [Status]() ```go func (r GetJobByIDResponse) Status() string @@ -2420,7 +2444,7 @@ func (r GetJobByIDResponse) Status() string Status returns HTTPResponse.Status -### func \(GetJobByIDResponse\) [StatusCode]() +### func \(GetJobByIDResponse\) [StatusCode]() ```go func (r GetJobByIDResponse) StatusCode() int @@ -2429,7 +2453,7 @@ func (r GetJobByIDResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetJobParams]() +## type [GetJobParams]() GetJobParams defines parameters for GetJob. @@ -2447,7 +2471,7 @@ type GetJobParams struct { ``` -## type [GetJobParamsStatus]() +## type [GetJobParamsStatus]() GetJobParamsStatus defines parameters for GetJob. @@ -2468,7 +2492,7 @@ const ( ``` -## type [GetJobResponse]() +## type [GetJobResponse]() @@ -2485,7 +2509,7 @@ type GetJobResponse struct { ``` -### func [ParseGetJobResponse]() +### func [ParseGetJobResponse]() ```go func ParseGetJobResponse(rsp *http.Response) (*GetJobResponse, error) @@ -2494,7 +2518,7 @@ func ParseGetJobResponse(rsp *http.Response) (*GetJobResponse, error) ParseGetJobResponse parses an HTTP response from a GetJobWithResponse call -### func \(GetJobResponse\) [Status]() +### func \(GetJobResponse\) [Status]() ```go func (r GetJobResponse) Status() string @@ -2503,7 +2527,7 @@ func (r GetJobResponse) Status() string Status returns HTTPResponse.Status -### func \(GetJobResponse\) [StatusCode]() +### func \(GetJobResponse\) [StatusCode]() ```go func (r GetJobResponse) StatusCode() int @@ -2512,7 +2536,7 @@ func (r GetJobResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetJobStatusResponse]() +## type [GetJobStatusResponse]() @@ -2528,7 +2552,7 @@ type GetJobStatusResponse struct { ``` -### func [ParseGetJobStatusResponse]() +### func [ParseGetJobStatusResponse]() ```go func ParseGetJobStatusResponse(rsp *http.Response) (*GetJobStatusResponse, error) @@ -2537,7 +2561,7 @@ func ParseGetJobStatusResponse(rsp *http.Response) (*GetJobStatusResponse, error ParseGetJobStatusResponse parses an HTTP response from a GetJobStatusWithResponse call -### func \(GetJobStatusResponse\) [Status]() +### func \(GetJobStatusResponse\) [Status]() ```go func (r GetJobStatusResponse) Status() string @@ -2546,7 +2570,7 @@ func (r GetJobStatusResponse) Status() string Status returns HTTPResponse.Status -### func \(GetJobStatusResponse\) [StatusCode]() +### func \(GetJobStatusResponse\) [StatusCode]() ```go func (r GetJobStatusResponse) StatusCode() int @@ -2555,7 +2579,7 @@ func (r GetJobStatusResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetNodeDiskResponse]() +## type [GetNodeDiskResponse]() @@ -2572,7 +2596,7 @@ type GetNodeDiskResponse struct { ``` -### func [ParseGetNodeDiskResponse]() +### func [ParseGetNodeDiskResponse]() ```go func ParseGetNodeDiskResponse(rsp *http.Response) (*GetNodeDiskResponse, error) @@ -2581,7 +2605,7 @@ func ParseGetNodeDiskResponse(rsp *http.Response) (*GetNodeDiskResponse, error) ParseGetNodeDiskResponse parses an HTTP response from a GetNodeDiskWithResponse call -### func \(GetNodeDiskResponse\) [Status]() +### func \(GetNodeDiskResponse\) [Status]() ```go func (r GetNodeDiskResponse) Status() string @@ -2590,7 +2614,7 @@ func (r GetNodeDiskResponse) Status() string Status returns HTTPResponse.Status -### func \(GetNodeDiskResponse\) [StatusCode]() +### func \(GetNodeDiskResponse\) [StatusCode]() ```go func (r GetNodeDiskResponse) StatusCode() int @@ -2599,7 +2623,7 @@ func (r GetNodeDiskResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetNodeHostnameResponse]() +## type [GetNodeHostnameResponse]() @@ -2616,7 +2640,7 @@ type GetNodeHostnameResponse struct { ``` -### func [ParseGetNodeHostnameResponse]() +### func [ParseGetNodeHostnameResponse]() ```go func ParseGetNodeHostnameResponse(rsp *http.Response) (*GetNodeHostnameResponse, error) @@ -2625,7 +2649,7 @@ func ParseGetNodeHostnameResponse(rsp *http.Response) (*GetNodeHostnameResponse, ParseGetNodeHostnameResponse parses an HTTP response from a GetNodeHostnameWithResponse call -### func \(GetNodeHostnameResponse\) [Status]() +### func \(GetNodeHostnameResponse\) [Status]() ```go func (r GetNodeHostnameResponse) Status() string @@ -2634,7 +2658,7 @@ func (r GetNodeHostnameResponse) Status() string Status returns HTTPResponse.Status -### func \(GetNodeHostnameResponse\) [StatusCode]() +### func \(GetNodeHostnameResponse\) [StatusCode]() ```go func (r GetNodeHostnameResponse) StatusCode() int @@ -2643,7 +2667,7 @@ func (r GetNodeHostnameResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetNodeLoadResponse]() +## type [GetNodeLoadResponse]() @@ -2660,7 +2684,7 @@ type GetNodeLoadResponse struct { ``` -### func [ParseGetNodeLoadResponse]() +### func [ParseGetNodeLoadResponse]() ```go func ParseGetNodeLoadResponse(rsp *http.Response) (*GetNodeLoadResponse, error) @@ -2669,7 +2693,7 @@ func ParseGetNodeLoadResponse(rsp *http.Response) (*GetNodeLoadResponse, error) ParseGetNodeLoadResponse parses an HTTP response from a GetNodeLoadWithResponse call -### func \(GetNodeLoadResponse\) [Status]() +### func \(GetNodeLoadResponse\) [Status]() ```go func (r GetNodeLoadResponse) Status() string @@ -2678,7 +2702,7 @@ func (r GetNodeLoadResponse) Status() string Status returns HTTPResponse.Status -### func \(GetNodeLoadResponse\) [StatusCode]() +### func \(GetNodeLoadResponse\) [StatusCode]() ```go func (r GetNodeLoadResponse) StatusCode() int @@ -2687,7 +2711,7 @@ func (r GetNodeLoadResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetNodeMemoryResponse]() +## type [GetNodeMemoryResponse]() @@ -2704,7 +2728,7 @@ type GetNodeMemoryResponse struct { ``` -### func [ParseGetNodeMemoryResponse]() +### func [ParseGetNodeMemoryResponse]() ```go func ParseGetNodeMemoryResponse(rsp *http.Response) (*GetNodeMemoryResponse, error) @@ -2713,7 +2737,7 @@ func ParseGetNodeMemoryResponse(rsp *http.Response) (*GetNodeMemoryResponse, err ParseGetNodeMemoryResponse parses an HTTP response from a GetNodeMemoryWithResponse call -### func \(GetNodeMemoryResponse\) [Status]() +### func \(GetNodeMemoryResponse\) [Status]() ```go func (r GetNodeMemoryResponse) Status() string @@ -2722,7 +2746,7 @@ func (r GetNodeMemoryResponse) Status() string Status returns HTTPResponse.Status -### func \(GetNodeMemoryResponse\) [StatusCode]() +### func \(GetNodeMemoryResponse\) [StatusCode]() ```go func (r GetNodeMemoryResponse) StatusCode() int @@ -2731,7 +2755,7 @@ func (r GetNodeMemoryResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetNodeNetworkDNSByInterfaceResponse]() +## type [GetNodeNetworkDNSByInterfaceResponse]() @@ -2748,7 +2772,7 @@ type GetNodeNetworkDNSByInterfaceResponse struct { ``` -### func [ParseGetNodeNetworkDNSByInterfaceResponse]() +### func [ParseGetNodeNetworkDNSByInterfaceResponse]() ```go func ParseGetNodeNetworkDNSByInterfaceResponse(rsp *http.Response) (*GetNodeNetworkDNSByInterfaceResponse, error) @@ -2757,7 +2781,7 @@ func ParseGetNodeNetworkDNSByInterfaceResponse(rsp *http.Response) (*GetNodeNetw ParseGetNodeNetworkDNSByInterfaceResponse parses an HTTP response from a GetNodeNetworkDNSByInterfaceWithResponse call -### func \(GetNodeNetworkDNSByInterfaceResponse\) [Status]() +### func \(GetNodeNetworkDNSByInterfaceResponse\) [Status]() ```go func (r GetNodeNetworkDNSByInterfaceResponse) Status() string @@ -2766,7 +2790,7 @@ func (r GetNodeNetworkDNSByInterfaceResponse) Status() string Status returns HTTPResponse.Status -### func \(GetNodeNetworkDNSByInterfaceResponse\) [StatusCode]() +### func \(GetNodeNetworkDNSByInterfaceResponse\) [StatusCode]() ```go func (r GetNodeNetworkDNSByInterfaceResponse) StatusCode() int @@ -2775,7 +2799,7 @@ func (r GetNodeNetworkDNSByInterfaceResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetNodeOSResponse]() +## type [GetNodeOSResponse]() @@ -2792,7 +2816,7 @@ type GetNodeOSResponse struct { ``` -### func [ParseGetNodeOSResponse]() +### func [ParseGetNodeOSResponse]() ```go func ParseGetNodeOSResponse(rsp *http.Response) (*GetNodeOSResponse, error) @@ -2801,7 +2825,7 @@ func ParseGetNodeOSResponse(rsp *http.Response) (*GetNodeOSResponse, error) ParseGetNodeOSResponse parses an HTTP response from a GetNodeOSWithResponse call -### func \(GetNodeOSResponse\) [Status]() +### func \(GetNodeOSResponse\) [Status]() ```go func (r GetNodeOSResponse) Status() string @@ -2810,7 +2834,7 @@ func (r GetNodeOSResponse) Status() string Status returns HTTPResponse.Status -### func \(GetNodeOSResponse\) [StatusCode]() +### func \(GetNodeOSResponse\) [StatusCode]() ```go func (r GetNodeOSResponse) StatusCode() int @@ -2819,7 +2843,7 @@ func (r GetNodeOSResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetNodeStatusResponse]() +## type [GetNodeStatusResponse]() @@ -2836,7 +2860,7 @@ type GetNodeStatusResponse struct { ``` -### func [ParseGetNodeStatusResponse]() +### func [ParseGetNodeStatusResponse]() ```go func ParseGetNodeStatusResponse(rsp *http.Response) (*GetNodeStatusResponse, error) @@ -2845,7 +2869,7 @@ func ParseGetNodeStatusResponse(rsp *http.Response) (*GetNodeStatusResponse, err ParseGetNodeStatusResponse parses an HTTP response from a GetNodeStatusWithResponse call -### func \(GetNodeStatusResponse\) [Status]() +### func \(GetNodeStatusResponse\) [Status]() ```go func (r GetNodeStatusResponse) Status() string @@ -2854,7 +2878,7 @@ func (r GetNodeStatusResponse) Status() string Status returns HTTPResponse.Status -### func \(GetNodeStatusResponse\) [StatusCode]() +### func \(GetNodeStatusResponse\) [StatusCode]() ```go func (r GetNodeStatusResponse) StatusCode() int @@ -2863,7 +2887,7 @@ func (r GetNodeStatusResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetNodeUptimeResponse]() +## type [GetNodeUptimeResponse]() @@ -2880,7 +2904,7 @@ type GetNodeUptimeResponse struct { ``` -### func [ParseGetNodeUptimeResponse]() +### func [ParseGetNodeUptimeResponse]() ```go func ParseGetNodeUptimeResponse(rsp *http.Response) (*GetNodeUptimeResponse, error) @@ -2889,7 +2913,7 @@ func ParseGetNodeUptimeResponse(rsp *http.Response) (*GetNodeUptimeResponse, err ParseGetNodeUptimeResponse parses an HTTP response from a GetNodeUptimeWithResponse call -### func \(GetNodeUptimeResponse\) [Status]() +### func \(GetNodeUptimeResponse\) [Status]() ```go func (r GetNodeUptimeResponse) Status() string @@ -2898,7 +2922,7 @@ func (r GetNodeUptimeResponse) Status() string Status returns HTTPResponse.Status -### func \(GetNodeUptimeResponse\) [StatusCode]() +### func \(GetNodeUptimeResponse\) [StatusCode]() ```go func (r GetNodeUptimeResponse) StatusCode() int @@ -2907,7 +2931,7 @@ func (r GetNodeUptimeResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [GetVersionResponse]() +## type [GetVersionResponse]() @@ -2920,7 +2944,7 @@ type GetVersionResponse struct { ``` -### func [ParseGetVersionResponse]() +### func [ParseGetVersionResponse]() ```go func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) @@ -2929,7 +2953,7 @@ func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) ParseGetVersionResponse parses an HTTP response from a GetVersionWithResponse call -### func \(GetVersionResponse\) [Status]() +### func \(GetVersionResponse\) [Status]() ```go func (r GetVersionResponse) Status() string @@ -2938,7 +2962,7 @@ func (r GetVersionResponse) Status() string Status returns HTTPResponse.Status -### func \(GetVersionResponse\) [StatusCode]() +### func \(GetVersionResponse\) [StatusCode]() ```go func (r GetVersionResponse) StatusCode() int @@ -2947,7 +2971,7 @@ func (r GetVersionResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [HealthResponse]() +## type [HealthResponse]() HealthResponse defines model for HealthResponse. @@ -2959,7 +2983,7 @@ type HealthResponse struct { ``` -## type [Hostname]() +## type [Hostname]() Hostname defines model for Hostname. @@ -2968,7 +2992,7 @@ type Hostname = string ``` -## type [HostnameCollectionResponse]() +## type [HostnameCollectionResponse]() HostnameCollectionResponse defines model for HostnameCollectionResponse. @@ -2981,7 +3005,7 @@ type HostnameCollectionResponse struct { ``` -## type [HostnameResponse]() +## type [HostnameResponse]() HostnameResponse The hostname of the system. @@ -2999,7 +3023,7 @@ type HostnameResponse struct { ``` -## type [HttpRequestDoer]() +## type [HttpRequestDoer]() Doer performs HTTP requests. @@ -3012,7 +3036,7 @@ type HttpRequestDoer interface { ``` -## type [JobDetailResponse]() +## type [JobDetailResponse]() JobDetailResponse defines model for JobDetailResponse. @@ -3079,7 +3103,7 @@ type JobDetailResponse struct { ``` -## type [JobStats]() +## type [JobStats]() JobStats defines model for JobStats. @@ -3106,7 +3130,7 @@ type JobStats struct { ``` -## type [KVBucketInfo]() +## type [KVBucketInfo]() KVBucketInfo defines model for KVBucketInfo. @@ -3124,7 +3148,7 @@ type KVBucketInfo struct { ``` -## type [ListAgentsResponse]() +## type [ListAgentsResponse]() ListAgentsResponse defines model for ListAgentsResponse. @@ -3138,7 +3162,7 @@ type ListAgentsResponse struct { ``` -## type [ListAuditResponse]() +## type [ListAuditResponse]() ListAuditResponse defines model for ListAuditResponse. @@ -3153,7 +3177,7 @@ type ListAuditResponse struct { ``` -## type [ListJobsResponse]() +## type [ListJobsResponse]() ListJobsResponse defines model for ListJobsResponse. @@ -3167,7 +3191,7 @@ type ListJobsResponse struct { ``` -## type [LoadAverageResponse]() +## type [LoadAverageResponse]() LoadAverageResponse The system load averages for 1, 5, and 15 minutes. @@ -3185,7 +3209,7 @@ type LoadAverageResponse struct { ``` -## type [LoadCollectionResponse]() +## type [LoadCollectionResponse]() LoadCollectionResponse defines model for LoadCollectionResponse. @@ -3198,7 +3222,7 @@ type LoadCollectionResponse struct { ``` -## type [LoadResultItem]() +## type [LoadResultItem]() LoadResultItem defines model for LoadResultItem. @@ -3216,7 +3240,7 @@ type LoadResultItem struct { ``` -## type [MemoryCollectionResponse]() +## type [MemoryCollectionResponse]() MemoryCollectionResponse defines model for MemoryCollectionResponse. @@ -3229,7 +3253,7 @@ type MemoryCollectionResponse struct { ``` -## type [MemoryResponse]() +## type [MemoryResponse]() MemoryResponse Memory usage information. @@ -3247,7 +3271,7 @@ type MemoryResponse struct { ``` -## type [MemoryResultItem]() +## type [MemoryResultItem]() MemoryResultItem defines model for MemoryResultItem. @@ -3265,7 +3289,7 @@ type MemoryResultItem struct { ``` -## type [NATSInfo]() +## type [NATSInfo]() NATSInfo defines model for NATSInfo. @@ -3279,8 +3303,43 @@ type NATSInfo struct { } ``` + +## type [NetworkInterfaceResponse]() + +NetworkInterfaceResponse defines model for NetworkInterfaceResponse. + +```go +type NetworkInterfaceResponse struct { + // Family IP address family. + Family *NetworkInterfaceResponseFamily `json:"family,omitempty"` + Ipv4 *string `json:"ipv4,omitempty"` + Ipv6 *string `json:"ipv6,omitempty"` + Mac *string `json:"mac,omitempty"` + Name string `json:"name"` +} +``` + + +## type [NetworkInterfaceResponseFamily]() + +NetworkInterfaceResponseFamily IP address family. + +```go +type NetworkInterfaceResponseFamily string +``` + +Defines values for NetworkInterfaceResponseFamily. + +```go +const ( + Dual NetworkInterfaceResponseFamily = "dual" + Inet NetworkInterfaceResponseFamily = "inet" + Inet6 NetworkInterfaceResponseFamily = "inet6" +) +``` + -## type [NodeStatusCollectionResponse]() +## type [NodeStatusCollectionResponse]() NodeStatusCollectionResponse defines model for NodeStatusCollectionResponse. @@ -3293,7 +3352,7 @@ type NodeStatusCollectionResponse struct { ``` -## type [NodeStatusResponse]() +## type [NodeStatusResponse]() NodeStatusResponse defines model for NodeStatusResponse. @@ -3323,7 +3382,7 @@ type NodeStatusResponse struct { ``` -## type [OSInfoCollectionResponse]() +## type [OSInfoCollectionResponse]() OSInfoCollectionResponse defines model for OSInfoCollectionResponse. @@ -3336,7 +3395,7 @@ type OSInfoCollectionResponse struct { ``` -## type [OSInfoResponse]() +## type [OSInfoResponse]() OSInfoResponse Operating system information. @@ -3351,7 +3410,7 @@ type OSInfoResponse struct { ``` -## type [OSInfoResultItem]() +## type [OSInfoResultItem]() OSInfoResultItem defines model for OSInfoResultItem. @@ -3369,7 +3428,7 @@ type OSInfoResultItem struct { ``` -## type [PingCollectionResponse]() +## type [PingCollectionResponse]() PingCollectionResponse defines model for PingCollectionResponse. @@ -3382,7 +3441,7 @@ type PingCollectionResponse struct { ``` -## type [PingResponse]() +## type [PingResponse]() PingResponse defines model for PingResponse. @@ -3415,7 +3474,7 @@ type PingResponse struct { ``` -## type [PostJobJSONRequestBody]() +## type [PostJobJSONRequestBody]() PostJobJSONRequestBody defines body for PostJob for application/json ContentType. @@ -3424,7 +3483,7 @@ type PostJobJSONRequestBody = CreateJobRequest ``` -## type [PostJobResponse]() +## type [PostJobResponse]() @@ -3441,7 +3500,7 @@ type PostJobResponse struct { ``` -### func [ParsePostJobResponse]() +### func [ParsePostJobResponse]() ```go func ParsePostJobResponse(rsp *http.Response) (*PostJobResponse, error) @@ -3450,7 +3509,7 @@ func ParsePostJobResponse(rsp *http.Response) (*PostJobResponse, error) ParsePostJobResponse parses an HTTP response from a PostJobWithResponse call -### func \(PostJobResponse\) [Status]() +### func \(PostJobResponse\) [Status]() ```go func (r PostJobResponse) Status() string @@ -3459,7 +3518,7 @@ func (r PostJobResponse) Status() string Status returns HTTPResponse.Status -### func \(PostJobResponse\) [StatusCode]() +### func \(PostJobResponse\) [StatusCode]() ```go func (r PostJobResponse) StatusCode() int @@ -3468,7 +3527,7 @@ func (r PostJobResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [PostNodeCommandExecJSONRequestBody]() +## type [PostNodeCommandExecJSONRequestBody]() PostNodeCommandExecJSONRequestBody defines body for PostNodeCommandExec for application/json ContentType. @@ -3477,7 +3536,7 @@ type PostNodeCommandExecJSONRequestBody = CommandExecRequest ``` -## type [PostNodeCommandExecResponse]() +## type [PostNodeCommandExecResponse]() @@ -3494,7 +3553,7 @@ type PostNodeCommandExecResponse struct { ``` -### func [ParsePostNodeCommandExecResponse]() +### func [ParsePostNodeCommandExecResponse]() ```go func ParsePostNodeCommandExecResponse(rsp *http.Response) (*PostNodeCommandExecResponse, error) @@ -3503,7 +3562,7 @@ func ParsePostNodeCommandExecResponse(rsp *http.Response) (*PostNodeCommandExecR ParsePostNodeCommandExecResponse parses an HTTP response from a PostNodeCommandExecWithResponse call -### func \(PostNodeCommandExecResponse\) [Status]() +### func \(PostNodeCommandExecResponse\) [Status]() ```go func (r PostNodeCommandExecResponse) Status() string @@ -3512,7 +3571,7 @@ func (r PostNodeCommandExecResponse) Status() string Status returns HTTPResponse.Status -### func \(PostNodeCommandExecResponse\) [StatusCode]() +### func \(PostNodeCommandExecResponse\) [StatusCode]() ```go func (r PostNodeCommandExecResponse) StatusCode() int @@ -3521,7 +3580,7 @@ func (r PostNodeCommandExecResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [PostNodeCommandShellJSONRequestBody]() +## type [PostNodeCommandShellJSONRequestBody]() PostNodeCommandShellJSONRequestBody defines body for PostNodeCommandShell for application/json ContentType. @@ -3530,7 +3589,7 @@ type PostNodeCommandShellJSONRequestBody = CommandShellRequest ``` -## type [PostNodeCommandShellResponse]() +## type [PostNodeCommandShellResponse]() @@ -3547,7 +3606,7 @@ type PostNodeCommandShellResponse struct { ``` -### func [ParsePostNodeCommandShellResponse]() +### func [ParsePostNodeCommandShellResponse]() ```go func ParsePostNodeCommandShellResponse(rsp *http.Response) (*PostNodeCommandShellResponse, error) @@ -3556,7 +3615,7 @@ func ParsePostNodeCommandShellResponse(rsp *http.Response) (*PostNodeCommandShel ParsePostNodeCommandShellResponse parses an HTTP response from a PostNodeCommandShellWithResponse call -### func \(PostNodeCommandShellResponse\) [Status]() +### func \(PostNodeCommandShellResponse\) [Status]() ```go func (r PostNodeCommandShellResponse) Status() string @@ -3565,7 +3624,7 @@ func (r PostNodeCommandShellResponse) Status() string Status returns HTTPResponse.Status -### func \(PostNodeCommandShellResponse\) [StatusCode]() +### func \(PostNodeCommandShellResponse\) [StatusCode]() ```go func (r PostNodeCommandShellResponse) StatusCode() int @@ -3574,7 +3633,7 @@ func (r PostNodeCommandShellResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [PostNodeNetworkPingJSONBody]() +## type [PostNodeNetworkPingJSONBody]() PostNodeNetworkPingJSONBody defines parameters for PostNodeNetworkPing. @@ -3586,7 +3645,7 @@ type PostNodeNetworkPingJSONBody struct { ``` -## type [PostNodeNetworkPingJSONRequestBody]() +## type [PostNodeNetworkPingJSONRequestBody]() PostNodeNetworkPingJSONRequestBody defines body for PostNodeNetworkPing for application/json ContentType. @@ -3595,7 +3654,7 @@ type PostNodeNetworkPingJSONRequestBody PostNodeNetworkPingJSONBody ``` -## type [PostNodeNetworkPingResponse]() +## type [PostNodeNetworkPingResponse]() @@ -3612,7 +3671,7 @@ type PostNodeNetworkPingResponse struct { ``` -### func [ParsePostNodeNetworkPingResponse]() +### func [ParsePostNodeNetworkPingResponse]() ```go func ParsePostNodeNetworkPingResponse(rsp *http.Response) (*PostNodeNetworkPingResponse, error) @@ -3621,7 +3680,7 @@ func ParsePostNodeNetworkPingResponse(rsp *http.Response) (*PostNodeNetworkPingR ParsePostNodeNetworkPingResponse parses an HTTP response from a PostNodeNetworkPingWithResponse call -### func \(PostNodeNetworkPingResponse\) [Status]() +### func \(PostNodeNetworkPingResponse\) [Status]() ```go func (r PostNodeNetworkPingResponse) Status() string @@ -3630,7 +3689,7 @@ func (r PostNodeNetworkPingResponse) Status() string Status returns HTTPResponse.Status -### func \(PostNodeNetworkPingResponse\) [StatusCode]() +### func \(PostNodeNetworkPingResponse\) [StatusCode]() ```go func (r PostNodeNetworkPingResponse) StatusCode() int @@ -3639,7 +3698,7 @@ func (r PostNodeNetworkPingResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [PutNodeNetworkDNSJSONRequestBody]() +## type [PutNodeNetworkDNSJSONRequestBody]() PutNodeNetworkDNSJSONRequestBody defines body for PutNodeNetworkDNS for application/json ContentType. @@ -3648,7 +3707,7 @@ type PutNodeNetworkDNSJSONRequestBody = DNSConfigUpdateRequest ``` -## type [PutNodeNetworkDNSResponse]() +## type [PutNodeNetworkDNSResponse]() @@ -3665,7 +3724,7 @@ type PutNodeNetworkDNSResponse struct { ``` -### func [ParsePutNodeNetworkDNSResponse]() +### func [ParsePutNodeNetworkDNSResponse]() ```go func ParsePutNodeNetworkDNSResponse(rsp *http.Response) (*PutNodeNetworkDNSResponse, error) @@ -3674,7 +3733,7 @@ func ParsePutNodeNetworkDNSResponse(rsp *http.Response) (*PutNodeNetworkDNSRespo ParsePutNodeNetworkDNSResponse parses an HTTP response from a PutNodeNetworkDNSWithResponse call -### func \(PutNodeNetworkDNSResponse\) [Status]() +### func \(PutNodeNetworkDNSResponse\) [Status]() ```go func (r PutNodeNetworkDNSResponse) Status() string @@ -3683,7 +3742,7 @@ func (r PutNodeNetworkDNSResponse) Status() string Status returns HTTPResponse.Status -### func \(PutNodeNetworkDNSResponse\) [StatusCode]() +### func \(PutNodeNetworkDNSResponse\) [StatusCode]() ```go func (r PutNodeNetworkDNSResponse) StatusCode() int @@ -3692,7 +3751,7 @@ func (r PutNodeNetworkDNSResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [QueueStatsResponse]() +## type [QueueStatsResponse]() QueueStatsResponse defines model for QueueStatsResponse. @@ -3713,7 +3772,7 @@ type QueueStatsResponse struct { ``` -## type [ReadyResponse]() +## type [ReadyResponse]() ReadyResponse defines model for ReadyResponse. @@ -3728,7 +3787,7 @@ type ReadyResponse struct { ``` -## type [RequestEditorFn]() +## type [RequestEditorFn]() RequestEditorFn is the function signature for the RequestEditor callback function @@ -3737,7 +3796,7 @@ type RequestEditorFn func(ctx context.Context, req *http.Request) error ``` -## type [RetryJobByIDJSONRequestBody]() +## type [RetryJobByIDJSONRequestBody]() RetryJobByIDJSONRequestBody defines body for RetryJobByID for application/json ContentType. @@ -3746,7 +3805,7 @@ type RetryJobByIDJSONRequestBody = RetryJobRequest ``` -## type [RetryJobByIDResponse]() +## type [RetryJobByIDResponse]() @@ -3764,7 +3823,7 @@ type RetryJobByIDResponse struct { ``` -### func [ParseRetryJobByIDResponse]() +### func [ParseRetryJobByIDResponse]() ```go func ParseRetryJobByIDResponse(rsp *http.Response) (*RetryJobByIDResponse, error) @@ -3773,7 +3832,7 @@ func ParseRetryJobByIDResponse(rsp *http.Response) (*RetryJobByIDResponse, error ParseRetryJobByIDResponse parses an HTTP response from a RetryJobByIDWithResponse call -### func \(RetryJobByIDResponse\) [Status]() +### func \(RetryJobByIDResponse\) [Status]() ```go func (r RetryJobByIDResponse) Status() string @@ -3782,7 +3841,7 @@ func (r RetryJobByIDResponse) Status() string Status returns HTTPResponse.Status -### func \(RetryJobByIDResponse\) [StatusCode]() +### func \(RetryJobByIDResponse\) [StatusCode]() ```go func (r RetryJobByIDResponse) StatusCode() int @@ -3791,7 +3850,7 @@ func (r RetryJobByIDResponse) StatusCode() int StatusCode returns HTTPResponse.StatusCode -## type [RetryJobRequest]() +## type [RetryJobRequest]() RetryJobRequest defines model for RetryJobRequest. @@ -3803,7 +3862,7 @@ type RetryJobRequest struct { ``` -## type [StatusResponse]() +## type [StatusResponse]() StatusResponse defines model for StatusResponse. @@ -3835,7 +3894,7 @@ type StatusResponse struct { ``` -## type [StreamInfo]() +## type [StreamInfo]() StreamInfo defines model for StreamInfo. @@ -3856,7 +3915,7 @@ type StreamInfo struct { ``` -## type [UptimeCollectionResponse]() +## type [UptimeCollectionResponse]() UptimeCollectionResponse defines model for UptimeCollectionResponse. @@ -3869,7 +3928,7 @@ type UptimeCollectionResponse struct { ``` -## type [UptimeResponse]() +## type [UptimeResponse]() UptimeResponse System uptime information. diff --git a/docs/gen/osapi.md b/docs/gen/osapi.md index 55b3816..7597992 100644 --- a/docs/gen/osapi.md +++ b/docs/gen/osapi.md @@ -25,51 +25,180 @@ resp, err := client.Node.Exec(ctx, osapi.ExecRequest{ ## Index +- [type APIError](<#APIError>) + - [func \(e \*APIError\) Error\(\) string](<#APIError.Error>) +- [type Agent](<#Agent>) +- [type AgentJobResponse](<#AgentJobResponse>) +- [type AgentList](<#AgentList>) - [type AgentService](<#AgentService>) - - [func \(s \*AgentService\) Get\(ctx context.Context, hostname string\) \(\*gen.GetAgentDetailsResponse, error\)](<#AgentService.Get>) - - [func \(s \*AgentService\) List\(ctx context.Context\) \(\*gen.GetAgentResponse, error\)](<#AgentService.List>) + - [func \(s \*AgentService\) Get\(ctx context.Context, hostname string\) \(\*Response\[Agent\], error\)](<#AgentService.Get>) + - [func \(s \*AgentService\) List\(ctx context.Context\) \(\*Response\[AgentList\], error\)](<#AgentService.List>) +- [type AgentState](<#AgentState>) +- [type AgentStats](<#AgentStats>) +- [type AgentSummary](<#AgentSummary>) +- [type AuditEntry](<#AuditEntry>) +- [type AuditList](<#AuditList>) - [type AuditService](<#AuditService>) - - [func \(s \*AuditService\) Export\(ctx context.Context\) \(\*gen.GetAuditExportResponse, error\)](<#AuditService.Export>) - - [func \(s \*AuditService\) Get\(ctx context.Context, id string\) \(\*gen.GetAuditLogByIDResponse, error\)](<#AuditService.Get>) - - [func \(s \*AuditService\) List\(ctx context.Context, limit int, offset int\) \(\*gen.GetAuditLogsResponse, error\)](<#AuditService.List>) + - [func \(s \*AuditService\) Export\(ctx context.Context\) \(\*Response\[AuditList\], error\)](<#AuditService.Export>) + - [func \(s \*AuditService\) Get\(ctx context.Context, id string\) \(\*Response\[AuditEntry\], error\)](<#AuditService.Get>) + - [func \(s \*AuditService\) List\(ctx context.Context, limit int, offset int\) \(\*Response\[AuditList\], error\)](<#AuditService.List>) +- [type AuthError](<#AuthError>) + - [func \(e \*AuthError\) Unwrap\(\) error](<#AuthError.Unwrap>) - [type Client](<#Client>) - [func New\(baseURL string, bearerToken string, opts ...Option\) \*Client](<#New>) +- [type Collection](<#Collection>) +- [type CommandResult](<#CommandResult>) +- [type ComponentHealth](<#ComponentHealth>) +- [type ConsumerDetail](<#ConsumerDetail>) +- [type ConsumerStats](<#ConsumerStats>) +- [type DNSConfig](<#DNSConfig>) +- [type DNSUpdateResult](<#DNSUpdateResult>) +- [type Disk](<#Disk>) +- [type DiskResult](<#DiskResult>) - [type ExecRequest](<#ExecRequest>) - [type HealthService](<#HealthService>) - - [func \(s \*HealthService\) Liveness\(ctx context.Context\) \(\*gen.GetHealthResponse, error\)](<#HealthService.Liveness>) - - [func \(s \*HealthService\) Ready\(ctx context.Context\) \(\*gen.GetHealthReadyResponse, error\)](<#HealthService.Ready>) - - [func \(s \*HealthService\) Status\(ctx context.Context\) \(\*gen.GetHealthStatusResponse, error\)](<#HealthService.Status>) + - [func \(s \*HealthService\) Liveness\(ctx context.Context\) \(\*Response\[HealthStatus\], error\)](<#HealthService.Liveness>) + - [func \(s \*HealthService\) Ready\(ctx context.Context\) \(\*Response\[ReadyStatus\], error\)](<#HealthService.Ready>) + - [func \(s \*HealthService\) Status\(ctx context.Context\) \(\*Response\[SystemStatus\], error\)](<#HealthService.Status>) +- [type HealthStatus](<#HealthStatus>) +- [type HostnameResult](<#HostnameResult>) +- [type JobCreated](<#JobCreated>) +- [type JobDetail](<#JobDetail>) +- [type JobList](<#JobList>) - [type JobService](<#JobService>) - - [func \(s \*JobService\) Create\(ctx context.Context, operation map\[string\]interface\{\}, target string\) \(\*gen.PostJobResponse, error\)](<#JobService.Create>) - - [func \(s \*JobService\) Delete\(ctx context.Context, id string\) \(\*gen.DeleteJobByIDResponse, error\)](<#JobService.Delete>) - - [func \(s \*JobService\) Get\(ctx context.Context, id string\) \(\*gen.GetJobByIDResponse, error\)](<#JobService.Get>) - - [func \(s \*JobService\) List\(ctx context.Context, params ListParams\) \(\*gen.GetJobResponse, error\)](<#JobService.List>) - - [func \(s \*JobService\) QueueStats\(ctx context.Context\) \(\*gen.GetJobStatusResponse, error\)](<#JobService.QueueStats>) - - [func \(s \*JobService\) Retry\(ctx context.Context, id string, target string\) \(\*gen.RetryJobByIDResponse, error\)](<#JobService.Retry>) + - [func \(s \*JobService\) Create\(ctx context.Context, operation map\[string\]interface\{\}, target string\) \(\*Response\[JobCreated\], error\)](<#JobService.Create>) + - [func \(s \*JobService\) Delete\(ctx context.Context, id string\) error](<#JobService.Delete>) + - [func \(s \*JobService\) Get\(ctx context.Context, id string\) \(\*Response\[JobDetail\], error\)](<#JobService.Get>) + - [func \(s \*JobService\) List\(ctx context.Context, params ListParams\) \(\*Response\[JobList\], error\)](<#JobService.List>) + - [func \(s \*JobService\) QueueStats\(ctx context.Context\) \(\*Response\[QueueStats\], error\)](<#JobService.QueueStats>) + - [func \(s \*JobService\) Retry\(ctx context.Context, id string, target string\) \(\*Response\[JobCreated\], error\)](<#JobService.Retry>) +- [type JobStats](<#JobStats>) +- [type KVBucketInfo](<#KVBucketInfo>) - [type ListParams](<#ListParams>) +- [type LoadAverage](<#LoadAverage>) +- [type LoadResult](<#LoadResult>) +- [type Memory](<#Memory>) +- [type MemoryResult](<#MemoryResult>) - [type MetricsService](<#MetricsService>) - [func \(s \*MetricsService\) Get\(ctx context.Context\) \(string, error\)](<#MetricsService.Get>) +- [type NATSInfo](<#NATSInfo>) +- [type NetworkInterface](<#NetworkInterface>) - [type NodeService](<#NodeService>) - - [func \(s \*NodeService\) Disk\(ctx context.Context, target string\) \(\*gen.GetNodeDiskResponse, error\)](<#NodeService.Disk>) - - [func \(s \*NodeService\) Exec\(ctx context.Context, req ExecRequest\) \(\*gen.PostNodeCommandExecResponse, error\)](<#NodeService.Exec>) - - [func \(s \*NodeService\) GetDNS\(ctx context.Context, target string, interfaceName string\) \(\*gen.GetNodeNetworkDNSByInterfaceResponse, error\)](<#NodeService.GetDNS>) - - [func \(s \*NodeService\) Hostname\(ctx context.Context, target string\) \(\*gen.GetNodeHostnameResponse, error\)](<#NodeService.Hostname>) - - [func \(s \*NodeService\) Load\(ctx context.Context, target string\) \(\*gen.GetNodeLoadResponse, error\)](<#NodeService.Load>) - - [func \(s \*NodeService\) Memory\(ctx context.Context, target string\) \(\*gen.GetNodeMemoryResponse, error\)](<#NodeService.Memory>) - - [func \(s \*NodeService\) OS\(ctx context.Context, target string\) \(\*gen.GetNodeOSResponse, error\)](<#NodeService.OS>) - - [func \(s \*NodeService\) Ping\(ctx context.Context, target string, address string\) \(\*gen.PostNodeNetworkPingResponse, error\)](<#NodeService.Ping>) - - [func \(s \*NodeService\) Shell\(ctx context.Context, req ShellRequest\) \(\*gen.PostNodeCommandShellResponse, error\)](<#NodeService.Shell>) - - [func \(s \*NodeService\) Status\(ctx context.Context, target string\) \(\*gen.GetNodeStatusResponse, error\)](<#NodeService.Status>) - - [func \(s \*NodeService\) UpdateDNS\(ctx context.Context, target string, interfaceName string, servers \[\]string, searchDomains \[\]string\) \(\*gen.PutNodeNetworkDNSResponse, error\)](<#NodeService.UpdateDNS>) - - [func \(s \*NodeService\) Uptime\(ctx context.Context, target string\) \(\*gen.GetNodeUptimeResponse, error\)](<#NodeService.Uptime>) + - [func \(s \*NodeService\) Disk\(ctx context.Context, target string\) \(\*Response\[Collection\[DiskResult\]\], error\)](<#NodeService.Disk>) + - [func \(s \*NodeService\) Exec\(ctx context.Context, req ExecRequest\) \(\*Response\[Collection\[CommandResult\]\], error\)](<#NodeService.Exec>) + - [func \(s \*NodeService\) GetDNS\(ctx context.Context, target string, interfaceName string\) \(\*Response\[Collection\[DNSConfig\]\], error\)](<#NodeService.GetDNS>) + - [func \(s \*NodeService\) Hostname\(ctx context.Context, target string\) \(\*Response\[Collection\[HostnameResult\]\], error\)](<#NodeService.Hostname>) + - [func \(s \*NodeService\) Load\(ctx context.Context, target string\) \(\*Response\[Collection\[LoadResult\]\], error\)](<#NodeService.Load>) + - [func \(s \*NodeService\) Memory\(ctx context.Context, target string\) \(\*Response\[Collection\[MemoryResult\]\], error\)](<#NodeService.Memory>) + - [func \(s \*NodeService\) OS\(ctx context.Context, target string\) \(\*Response\[Collection\[OSInfoResult\]\], error\)](<#NodeService.OS>) + - [func \(s \*NodeService\) Ping\(ctx context.Context, target string, address string\) \(\*Response\[Collection\[PingResult\]\], error\)](<#NodeService.Ping>) + - [func \(s \*NodeService\) Shell\(ctx context.Context, req ShellRequest\) \(\*Response\[Collection\[CommandResult\]\], error\)](<#NodeService.Shell>) + - [func \(s \*NodeService\) Status\(ctx context.Context, target string\) \(\*Response\[Collection\[NodeStatus\]\], error\)](<#NodeService.Status>) + - [func \(s \*NodeService\) UpdateDNS\(ctx context.Context, target string, interfaceName string, servers \[\]string, searchDomains \[\]string\) \(\*Response\[Collection\[DNSUpdateResult\]\], error\)](<#NodeService.UpdateDNS>) + - [func \(s \*NodeService\) Uptime\(ctx context.Context, target string\) \(\*Response\[Collection\[UptimeResult\]\], error\)](<#NodeService.Uptime>) +- [type NodeStatus](<#NodeStatus>) +- [type NotFoundError](<#NotFoundError>) + - [func \(e \*NotFoundError\) Unwrap\(\) error](<#NotFoundError.Unwrap>) +- [type OSInfo](<#OSInfo>) +- [type OSInfoResult](<#OSInfoResult>) - [type Option](<#Option>) - [func WithHTTPTransport\(transport http.RoundTripper\) Option](<#WithHTTPTransport>) - [func WithLogger\(logger \*slog.Logger\) Option](<#WithLogger>) +- [type PingResult](<#PingResult>) +- [type QueueStats](<#QueueStats>) +- [type ReadyStatus](<#ReadyStatus>) +- [type Response](<#Response>) + - [func NewResponse\[T any\]\(data T, rawJSON \[\]byte\) \*Response\[T\]](<#NewResponse>) + - [func \(r \*Response\[T\]\) RawJSON\(\) \[\]byte](<#Response[T].RawJSON>) +- [type ServerError](<#ServerError>) + - [func \(e \*ServerError\) Unwrap\(\) error](<#ServerError.Unwrap>) - [type ShellRequest](<#ShellRequest>) +- [type StreamInfo](<#StreamInfo>) +- [type SystemStatus](<#SystemStatus>) +- [type TimelineEvent](<#TimelineEvent>) +- [type UnexpectedStatusError](<#UnexpectedStatusError>) + - [func \(e \*UnexpectedStatusError\) Unwrap\(\) error](<#UnexpectedStatusError.Unwrap>) +- [type UptimeResult](<#UptimeResult>) +- [type ValidationError](<#ValidationError>) + - [func \(e \*ValidationError\) Unwrap\(\) error](<#ValidationError.Unwrap>) + +## type [APIError]() + +APIError is the base error type for OSAPI API errors. + +```go +type APIError struct { + StatusCode int + Message string +} +``` + + +### func \(\*APIError\) [Error]() + +```go +func (e *APIError) Error() string +``` + +Error returns a formatted error string. + + +## type [Agent]() + +Agent represents a registered OSAPI agent. + +```go +type Agent struct { + Hostname string + Status string + Labels map[string]string + Architecture string + CPUCount int + Fqdn string + KernelVersion string + PackageMgr string + ServiceMgr string + LoadAverage *LoadAverage + Memory *Memory + OSInfo *OSInfo + Interfaces []NetworkInterface + Uptime string + StartedAt time.Time + RegisteredAt time.Time + Facts map[string]any +} +``` + + +## type [AgentJobResponse]() + +AgentJobResponse represents an agent's response data for a broadcast job. + +```go +type AgentJobResponse struct { + Hostname string + Status string + Error string + Data any +} +``` + + +## type [AgentList]() + +AgentList is a collection of agents. + +```go +type AgentList struct { + Agents []Agent + Total int +} +``` + -## type [AgentService]() +## type [AgentService]() AgentService provides agent discovery and details operations. @@ -80,25 +209,96 @@ type AgentService struct { ``` -### func \(\*AgentService\) [Get]() +### func \(\*AgentService\) [Get]() ```go -func (s *AgentService) Get(ctx context.Context, hostname string) (*gen.GetAgentDetailsResponse, error) +func (s *AgentService) Get(ctx context.Context, hostname string) (*Response[Agent], error) ``` Get retrieves detailed information about a specific agent by hostname. -### func \(\*AgentService\) [List]() +### func \(\*AgentService\) [List]() ```go -func (s *AgentService) List(ctx context.Context) (*gen.GetAgentResponse, error) +func (s *AgentService) List(ctx context.Context) (*Response[AgentList], error) ``` List retrieves all active agents. + +## type [AgentState]() + +AgentState represents an agent's processing state for a broadcast job. + +```go +type AgentState struct { + Status string + Duration string + Error string +} +``` + + +## type [AgentStats]() + +AgentStats represents agent statistics from the health endpoint. + +```go +type AgentStats struct { + Total int + Ready int + Agents []AgentSummary +} +``` + + +## type [AgentSummary]() + +AgentSummary represents a summary of an agent from the health endpoint. + +```go +type AgentSummary struct { + Hostname string + Labels string + Registered string +} +``` + + +## type [AuditEntry]() + +AuditEntry represents a single audit log entry. + +```go +type AuditEntry struct { + ID string + Timestamp time.Time + User string + Roles []string + Method string + Path string + ResponseCode int + DurationMs int64 + SourceIP string + OperationID string +} +``` + + +## type [AuditList]() + +AuditList is a paginated list of audit entries. + +```go +type AuditList struct { + Items []AuditEntry + TotalItems int +} +``` + -## type [AuditService]() +## type [AuditService]() AuditService provides audit log operations. @@ -109,32 +309,52 @@ type AuditService struct { ``` -### func \(\*AuditService\) [Export]() +### func \(\*AuditService\) [Export]() ```go -func (s *AuditService) Export(ctx context.Context) (*gen.GetAuditExportResponse, error) +func (s *AuditService) Export(ctx context.Context) (*Response[AuditList], error) ``` Export retrieves all audit log entries for export. -### func \(\*AuditService\) [Get]() +### func \(\*AuditService\) [Get]() ```go -func (s *AuditService) Get(ctx context.Context, id string) (*gen.GetAuditLogByIDResponse, error) +func (s *AuditService) Get(ctx context.Context, id string) (*Response[AuditEntry], error) ``` Get retrieves a single audit log entry by ID. -### func \(\*AuditService\) [List]() +### func \(\*AuditService\) [List]() ```go -func (s *AuditService) List(ctx context.Context, limit int, offset int) (*gen.GetAuditLogsResponse, error) +func (s *AuditService) List(ctx context.Context, limit int, offset int) (*Response[AuditList], error) ``` List retrieves audit log entries with pagination. + +## type [AuthError]() + +AuthError represents authentication/authorization errors \(401, 403\). + +```go +type AuthError struct { + APIError +} +``` + + +### func \(\*AuthError\) [Unwrap]() + +```go +func (e *AuthError) Unwrap() error +``` + +Unwrap returns the underlying APIError. + ## type [Client]() @@ -173,8 +393,130 @@ func New(baseURL string, bearerToken string, opts ...Option) *Client New creates an OSAPI SDK client. + +## type [Collection]() + +Collection is a generic wrapper for collection responses from node queries. + +```go +type Collection[T any] struct { + Results []T + JobID string +} +``` + + +## type [CommandResult]() + +CommandResult represents command execution result from a single agent. + +```go +type CommandResult struct { + Hostname string + Stdout string + Stderr string + Error string + ExitCode int + Changed bool + DurationMs int64 +} +``` + + +## type [ComponentHealth]() + +ComponentHealth represents a component's health. + +```go +type ComponentHealth struct { + Status string + Error string +} +``` + + +## type [ConsumerDetail]() + +ConsumerDetail represents a single consumer's details. + +```go +type ConsumerDetail struct { + Name string + Pending int + AckPending int + Redelivered int +} +``` + + +## type [ConsumerStats]() + +ConsumerStats represents JetStream consumer statistics. + +```go +type ConsumerStats struct { + Total int + Consumers []ConsumerDetail +} +``` + + +## type [DNSConfig]() + +DNSConfig represents DNS configuration from a single agent. + +```go +type DNSConfig struct { + Hostname string + Error string + Servers []string + SearchDomains []string +} +``` + + +## type [DNSUpdateResult]() + +DNSUpdateResult represents DNS update result from a single agent. + +```go +type DNSUpdateResult struct { + Hostname string + Status string + Error string + Changed bool +} +``` + + +## type [Disk]() + +Disk represents disk usage information. + +```go +type Disk struct { + Name string + Total int + Used int + Free int +} +``` + + +## type [DiskResult]() + +DiskResult represents disk query result from a single agent. + +```go +type DiskResult struct { + Hostname string + Error string + Disks []Disk +} +``` + -## type [ExecRequest]() +## type [ExecRequest]() ExecRequest contains parameters for direct command execution. @@ -199,7 +541,7 @@ type ExecRequest struct { ``` -## type [HealthService]() +## type [HealthService]() HealthService provides health check operations. @@ -210,31 +552,102 @@ type HealthService struct { ``` -### func \(\*HealthService\) [Liveness]() +### func \(\*HealthService\) [Liveness]() ```go -func (s *HealthService) Liveness(ctx context.Context) (*gen.GetHealthResponse, error) +func (s *HealthService) Liveness(ctx context.Context) (*Response[HealthStatus], error) ``` Liveness checks if the API server process is alive. -### func \(\*HealthService\) [Ready]() +### func \(\*HealthService\) [Ready]() ```go -func (s *HealthService) Ready(ctx context.Context) (*gen.GetHealthReadyResponse, error) +func (s *HealthService) Ready(ctx context.Context) (*Response[ReadyStatus], error) ``` -Ready checks if the API server and its dependencies are ready to serve traffic. +Ready checks if the API server and its dependencies are ready to serve traffic. A 503 response is treated as success with the ServiceUnavailable flag set. -### func \(\*HealthService\) [Status]() +### func \(\*HealthService\) [Status]() + +```go +func (s *HealthService) Status(ctx context.Context) (*Response[SystemStatus], error) +``` + +Status returns detailed system status including component health, NATS info, stream stats, and job queue counts. Requires authentication. A 503 response is treated as success with the ServiceUnavailable flag set. + + +## type [HealthStatus]() + +HealthStatus represents a liveness check response. + +```go +type HealthStatus struct { + Status string +} +``` + + +## type [HostnameResult]() + +HostnameResult represents a hostname query result from a single agent. + +```go +type HostnameResult struct { + Hostname string + Error string + Labels map[string]string +} +``` + + +## type [JobCreated]() + +JobCreated represents a newly created job response. + +```go +type JobCreated struct { + JobID string + Status string + Revision int64 + Timestamp string +} +``` + + +## type [JobDetail]() + +JobDetail represents a job's full details. ```go -func (s *HealthService) Status(ctx context.Context) (*gen.GetHealthStatusResponse, error) +type JobDetail struct { + ID string + Status string + Hostname string + Created string + UpdatedAt string + Error string + Operation map[string]any + Result any + AgentStates map[string]AgentState + Responses map[string]AgentJobResponse + Timeline []TimelineEvent +} ``` -Status returns detailed system status including component health, NATS info, stream stats, and job queue counts. Requires authentication. + +## type [JobList]() + +JobList is a paginated list of jobs. + +```go +type JobList struct { + Items []JobDetail + TotalItems int +} +``` ## type [JobService]() @@ -251,58 +664,87 @@ type JobService struct { ### func \(\*JobService\) [Create]() ```go -func (s *JobService) Create(ctx context.Context, operation map[string]interface{}, target string) (*gen.PostJobResponse, error) +func (s *JobService) Create(ctx context.Context, operation map[string]interface{}, target string) (*Response[JobCreated], error) ``` Create creates a new job with the given operation and target. -### func \(\*JobService\) [Delete]() +### func \(\*JobService\) [Delete]() ```go -func (s *JobService) Delete(ctx context.Context, id string) (*gen.DeleteJobByIDResponse, error) +func (s *JobService) Delete(ctx context.Context, id string) error ``` Delete deletes a job by ID. -### func \(\*JobService\) [Get]() +### func \(\*JobService\) [Get]() ```go -func (s *JobService) Get(ctx context.Context, id string) (*gen.GetJobByIDResponse, error) +func (s *JobService) Get(ctx context.Context, id string) (*Response[JobDetail], error) ``` Get retrieves a job by ID. -### func \(\*JobService\) [List]() +### func \(\*JobService\) [List]() ```go -func (s *JobService) List(ctx context.Context, params ListParams) (*gen.GetJobResponse, error) +func (s *JobService) List(ctx context.Context, params ListParams) (*Response[JobList], error) ``` List retrieves jobs, optionally filtered by status. -### func \(\*JobService\) [QueueStats]() +### func \(\*JobService\) [QueueStats]() ```go -func (s *JobService) QueueStats(ctx context.Context) (*gen.GetJobStatusResponse, error) +func (s *JobService) QueueStats(ctx context.Context) (*Response[QueueStats], error) ``` QueueStats retrieves job queue statistics. -### func \(\*JobService\) [Retry]() +### func \(\*JobService\) [Retry]() ```go -func (s *JobService) Retry(ctx context.Context, id string, target string) (*gen.RetryJobByIDResponse, error) +func (s *JobService) Retry(ctx context.Context, id string, target string) (*Response[JobCreated], error) ``` Retry retries a failed job by ID, optionally on a different target. + +## type [JobStats]() + +JobStats represents job queue statistics from the health endpoint. + +```go +type JobStats struct { + Total int + Completed int + Failed int + Processing int + Unprocessed int + Dlq int +} +``` + + +## type [KVBucketInfo]() + +KVBucketInfo represents a KV bucket's info. + +```go +type KVBucketInfo struct { + Name string + Keys int + Bytes int +} +``` + -## type [ListParams]() +## type [ListParams]() ListParams contains optional filters for listing jobs. @@ -320,6 +762,58 @@ type ListParams struct { } ``` + +## type [LoadAverage]() + +LoadAverage represents system load averages. + +```go +type LoadAverage struct { + OneMin float32 + FiveMin float32 + FifteenMin float32 +} +``` + + +## type [LoadResult]() + +LoadResult represents load average query result from a single agent. + +```go +type LoadResult struct { + Hostname string + Error string + LoadAverage *LoadAverage +} +``` + + +## type [Memory]() + +Memory represents memory usage information. + +```go +type Memory struct { + Total int + Used int + Free int +} +``` + + +## type [MemoryResult]() + +MemoryResult represents memory query result from a single agent. + +```go +type MemoryResult struct { + Hostname string + Error string + Memory *Memory +} +``` + ## type [MetricsService]() @@ -340,8 +834,35 @@ func (s *MetricsService) Get(ctx context.Context) (string, error) Get fetches the raw Prometheus metrics text from the /metrics endpoint. + +## type [NATSInfo]() + +NATSInfo represents NATS connection info. + +```go +type NATSInfo struct { + URL string + Version string +} +``` + + +## type [NetworkInterface]() + +NetworkInterface represents a network interface on an agent. + +```go +type NetworkInterface struct { + Name string + Family string + IPv4 string + IPv6 string + MAC string +} +``` + -## type [NodeService]() +## type [NodeService]() NodeService provides node management operations. @@ -352,113 +873,175 @@ type NodeService struct { ``` -### func \(\*NodeService\) [Disk]() +### func \(\*NodeService\) [Disk]() ```go -func (s *NodeService) Disk(ctx context.Context, target string) (*gen.GetNodeDiskResponse, error) +func (s *NodeService) Disk(ctx context.Context, target string) (*Response[Collection[DiskResult]], error) ``` Disk retrieves disk usage information from the target host. -### func \(\*NodeService\) [Exec]() +### func \(\*NodeService\) [Exec]() ```go -func (s *NodeService) Exec(ctx context.Context, req ExecRequest) (*gen.PostNodeCommandExecResponse, error) +func (s *NodeService) Exec(ctx context.Context, req ExecRequest) (*Response[Collection[CommandResult]], error) ``` Exec executes a command directly without a shell interpreter. -### func \(\*NodeService\) [GetDNS]() +### func \(\*NodeService\) [GetDNS]() ```go -func (s *NodeService) GetDNS(ctx context.Context, target string, interfaceName string) (*gen.GetNodeNetworkDNSByInterfaceResponse, error) +func (s *NodeService) GetDNS(ctx context.Context, target string, interfaceName string) (*Response[Collection[DNSConfig]], error) ``` GetDNS retrieves DNS configuration for a network interface on the target host. -### func \(\*NodeService\) [Hostname]() +### func \(\*NodeService\) [Hostname]() ```go -func (s *NodeService) Hostname(ctx context.Context, target string) (*gen.GetNodeHostnameResponse, error) +func (s *NodeService) Hostname(ctx context.Context, target string) (*Response[Collection[HostnameResult]], error) ``` Hostname retrieves the hostname from the target host. -### func \(\*NodeService\) [Load]() +### func \(\*NodeService\) [Load]() ```go -func (s *NodeService) Load(ctx context.Context, target string) (*gen.GetNodeLoadResponse, error) +func (s *NodeService) Load(ctx context.Context, target string) (*Response[Collection[LoadResult]], error) ``` Load retrieves load average information from the target host. -### func \(\*NodeService\) [Memory]() +### func \(\*NodeService\) [Memory]() ```go -func (s *NodeService) Memory(ctx context.Context, target string) (*gen.GetNodeMemoryResponse, error) +func (s *NodeService) Memory(ctx context.Context, target string) (*Response[Collection[MemoryResult]], error) ``` Memory retrieves memory usage information from the target host. -### func \(\*NodeService\) [OS]() +### func \(\*NodeService\) [OS]() ```go -func (s *NodeService) OS(ctx context.Context, target string) (*gen.GetNodeOSResponse, error) +func (s *NodeService) OS(ctx context.Context, target string) (*Response[Collection[OSInfoResult]], error) ``` OS retrieves operating system information from the target host. -### func \(\*NodeService\) [Ping]() +### func \(\*NodeService\) [Ping]() ```go -func (s *NodeService) Ping(ctx context.Context, target string, address string) (*gen.PostNodeNetworkPingResponse, error) +func (s *NodeService) Ping(ctx context.Context, target string, address string) (*Response[Collection[PingResult]], error) ``` Ping sends an ICMP ping to the specified address from the target host. -### func \(\*NodeService\) [Shell]() +### func \(\*NodeService\) [Shell]() ```go -func (s *NodeService) Shell(ctx context.Context, req ShellRequest) (*gen.PostNodeCommandShellResponse, error) +func (s *NodeService) Shell(ctx context.Context, req ShellRequest) (*Response[Collection[CommandResult]], error) ``` Shell executes a command through /bin/sh \-c with shell features \(pipes, redirects, variable expansion\). -### func \(\*NodeService\) [Status]() +### func \(\*NodeService\) [Status]() ```go -func (s *NodeService) Status(ctx context.Context, target string) (*gen.GetNodeStatusResponse, error) +func (s *NodeService) Status(ctx context.Context, target string) (*Response[Collection[NodeStatus]], error) ``` Status retrieves node status \(OS info, disk, memory, load\) from the target host. -### func \(\*NodeService\) [UpdateDNS]() +### func \(\*NodeService\) [UpdateDNS]() ```go -func (s *NodeService) UpdateDNS(ctx context.Context, target string, interfaceName string, servers []string, searchDomains []string) (*gen.PutNodeNetworkDNSResponse, error) +func (s *NodeService) UpdateDNS(ctx context.Context, target string, interfaceName string, servers []string, searchDomains []string) (*Response[Collection[DNSUpdateResult]], error) ``` UpdateDNS updates DNS configuration for a network interface on the target host. -### func \(\*NodeService\) [Uptime]() +### func \(\*NodeService\) [Uptime]() ```go -func (s *NodeService) Uptime(ctx context.Context, target string) (*gen.GetNodeUptimeResponse, error) +func (s *NodeService) Uptime(ctx context.Context, target string) (*Response[Collection[UptimeResult]], error) ``` Uptime retrieves uptime information from the target host. + +## type [NodeStatus]() + +NodeStatus represents full node status from a single agent. + +```go +type NodeStatus struct { + Hostname string + Uptime string + Error string + Disks []Disk + LoadAverage *LoadAverage + Memory *Memory + OSInfo *OSInfo +} +``` + + +## type [NotFoundError]() + +NotFoundError represents resource not found errors \(404\). + +```go +type NotFoundError struct { + APIError +} +``` + + +### func \(\*NotFoundError\) [Unwrap]() + +```go +func (e *NotFoundError) Unwrap() error +``` + +Unwrap returns the underlying APIError. + + +## type [OSInfo]() + +OSInfo represents operating system information. + +```go +type OSInfo struct { + Distribution string + Version string +} +``` + + +## type [OSInfoResult]() + +OSInfoResult represents OS info query result from a single agent. + +```go +type OSInfoResult struct { + Hostname string + Error string + OSInfo *OSInfo +} +``` + ## type [Option]() @@ -486,8 +1069,103 @@ func WithLogger(logger *slog.Logger) Option WithLogger sets a custom logger. Defaults to slog.Default\(\). + +## type [PingResult]() + +PingResult represents ping result from a single agent. + +```go +type PingResult struct { + Hostname string + Error string + PacketsSent int + PacketsReceived int + PacketLoss float64 + MinRtt string + AvgRtt string + MaxRtt string +} +``` + + +## type [QueueStats]() + +QueueStats represents job queue statistics. + +```go +type QueueStats struct { + TotalJobs int + DlqCount int + StatusCounts map[string]int + OperationCounts map[string]int +} +``` + + +## type [ReadyStatus]() + +ReadyStatus represents a readiness check response. + +```go +type ReadyStatus struct { + Status string + Error string + ServiceUnavailable bool +} +``` + + +## type [Response]() + +Response wraps a domain type with raw JSON for CLI \-\-json mode. + +```go +type Response[T any] struct { + Data T + // contains filtered or unexported fields +} +``` + + +### func [NewResponse]() + +```go +func NewResponse[T any](data T, rawJSON []byte) *Response[T] +``` + +NewResponse creates a Response with the given data and raw JSON body. + + +### func \(\*Response\[T\]\) [RawJSON]() + +```go +func (r *Response[T]) RawJSON() []byte +``` + +RawJSON returns the raw HTTP response body. + + +## type [ServerError]() + +ServerError represents internal server errors \(500\). + +```go +type ServerError struct { + APIError +} +``` + + +### func \(\*ServerError\) [Unwrap]() + +```go +func (e *ServerError) Unwrap() error +``` + +Unwrap returns the underlying APIError. + -## type [ShellRequest]() +## type [ShellRequest]() ShellRequest contains parameters for shell command execution. @@ -508,4 +1186,107 @@ type ShellRequest struct { } ``` + +## type [StreamInfo]() + +StreamInfo represents a JetStream stream's info. + +```go +type StreamInfo struct { + Name string + Messages int + Bytes int + Consumers int +} +``` + + +## type [SystemStatus]() + +SystemStatus represents detailed system status. + +```go +type SystemStatus struct { + Status string + Version string + Uptime string + ServiceUnavailable bool + Components map[string]ComponentHealth + NATS *NATSInfo + Agents *AgentStats + Jobs *JobStats + Consumers *ConsumerStats + Streams []StreamInfo + KVBuckets []KVBucketInfo +} +``` + + +## type [TimelineEvent]() + +TimelineEvent represents a job lifecycle event. + +```go +type TimelineEvent struct { + Timestamp string + Event string + Hostname string + Message string + Error string +} +``` + + +## type [UnexpectedStatusError]() + +UnexpectedStatusError represents unexpected HTTP status codes. + +```go +type UnexpectedStatusError struct { + APIError +} +``` + + +### func \(\*UnexpectedStatusError\) [Unwrap]() + +```go +func (e *UnexpectedStatusError) Unwrap() error +``` + +Unwrap returns the underlying APIError. + + +## type [UptimeResult]() + +UptimeResult represents uptime query result from a single agent. + +```go +type UptimeResult struct { + Hostname string + Uptime string + Error string +} +``` + + +## type [ValidationError]() + +ValidationError represents validation errors \(400\). + +```go +type ValidationError struct { + APIError +} +``` + + +### func \(\*ValidationError\) [Unwrap]() + +```go +func (e *ValidationError) Unwrap() error +``` + +Unwrap returns the underlying APIError. + Generated by [gomarkdoc]() diff --git a/docs/orchestration/README.md b/docs/orchestration/README.md index 2bbe214..412b985 100644 --- a/docs/orchestration/README.md +++ b/docs/orchestration/README.md @@ -20,6 +20,7 @@ to an OSAPI job type that agents execute. | [`node.status.get`](node-status.md) | Get node status | Read-only | Node | | [`node.disk.get`](node-disk.md) | Get disk usage | Read-only | Node | | [`node.memory.get`](node-memory.md) | Get memory stats | Read-only | Node | +| [`node.uptime.get`](node-uptime.md) | Get system uptime | Read-only | Node | | [`node.load.get`](node-load.md) | Get load averages | Read-only | Node | ### Idempotency diff --git a/docs/orchestration/node-uptime.md b/docs/orchestration/node-uptime.md new file mode 100644 index 0000000..5e8fec2 --- /dev/null +++ b/docs/orchestration/node-uptime.md @@ -0,0 +1,29 @@ +# node.uptime.get + +Get system uptime information. + +## Usage + +```go +task := plan.Task("get-uptime", &orchestrator.Op{ + Operation: "node.uptime.get", + Target: "_any", +}) +``` + +## Parameters + +None. + +## Target + +Accepts any valid target: `_any`, `_all`, a hostname, or a label selector +(`key:value`). + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `node:read` permission. diff --git a/examples/all/main.go b/examples/all/main.go deleted file mode 100644 index 9c2fabe..0000000 --- a/examples/all/main.go +++ /dev/null @@ -1,381 +0,0 @@ -// Copyright (c) 2026 John Dewey - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -// Package main demonstrates every orchestrator feature: hooks for consumer- -// controlled logging at every lifecycle point, Op and TaskFunc tasks, -// TaskFuncWithResults for inter-task data passing, dependencies, guards -// with Status inspection, Levels() for DAG inspection, error strategies -// (plan-level Continue + per-task Retry), parameterized operations, -// result data access, and detailed result reporting with Data. -// -// This example serves as a reference for building tools like Terraform -// or Ansible that consume the SDK. -// -// DAG: -// -// check-health -// ├── get-hostname ────────────┐ -// ├── get-disk │ -// ├── get-memory ├── print-summary (TaskFuncWithResults, reads prior data) -// ├── get-load [retry:2] ──────┘ -// └── run-uptime [params] ─────┘ -// optional-fail [continue] (independent) -// └── alert-on-failure (When: checks Status == StatusFailed) -// -// Run with: OSAPI_TOKEN="" go run main.go -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - "os" - "strings" - - "github.com/osapi-io/osapi-sdk/pkg/orchestrator" - "github.com/osapi-io/osapi-sdk/pkg/osapi" -) - -func main() { - token := os.Getenv("OSAPI_TOKEN") - if token == "" { - log.Fatal("OSAPI_TOKEN is required") - } - - url := os.Getenv("OSAPI_URL") - if url == "" { - url = "http://localhost:8080" - } - - client := osapi.New(url, token) - - // --- Hooks: consumer-controlled logging at every stage --- - - hooks := orchestrator.Hooks{ - BeforePlan: func(summary orchestrator.PlanSummary) { - fmt.Println("=== Execution Plan ===") - fmt.Printf("Plan: %d tasks, %d steps\n", summary.TotalTasks, len(summary.Steps)) - - for i, step := range summary.Steps { - mode := "sequential" - if step.Parallel { - mode = "parallel" - } - - fmt.Printf( - "\nStep %d (%s): %s\n", - i+1, - mode, - strings.Join(step.Tasks, ", "), - ) - } - - fmt.Println() - }, - AfterPlan: func(report *orchestrator.Report) { - fmt.Printf( - "\n=== Complete: %s in %s ===\n", - report.Summary(), - report.Duration, - ) - }, - BeforeLevel: func( - level int, - tasks []*orchestrator.Task, - parallel bool, - ) { - names := make([]string, len(tasks)) - for i, t := range tasks { - names[i] = t.Name() - } - - mode := "sequential" - if parallel { - mode = "parallel" - } - - fmt.Printf( - "\n>>> Step %d (%s): %s\n", - level+1, - mode, - strings.Join(names, ", "), - ) - }, - AfterLevel: func(level int, results []orchestrator.TaskResult) { - changed := 0 - for _, r := range results { - if r.Changed { - changed++ - } - } - - fmt.Printf( - "<<< Step %d done: %d/%d changed\n", - level+1, - changed, - len(results), - ) - }, - BeforeTask: func(task *orchestrator.Task) { - if op := task.Operation(); op != nil { - fmt.Printf( - " [start] %s operation=%s target=%s\n", - task.Name(), - op.Operation, - op.Target, - ) - } else { - fmt.Printf(" [start] %s (custom function)\n", task.Name()) - } - }, - AfterTask: func( - _ *orchestrator.Task, - result orchestrator.TaskResult, - ) { - fmt.Printf( - " [%s] %s changed=%v duration=%s\n", - result.Status, - result.Name, - result.Changed, - result.Duration, - ) - }, - OnRetry: func( - task *orchestrator.Task, - attempt int, - err error, - ) { - fmt.Printf( - " [retry] %s attempt=%d error=%q\n", - task.Name(), - attempt, - err, - ) - }, - OnSkip: func(task *orchestrator.Task, reason string) { - fmt.Printf(" [skip] %s reason=%q\n", task.Name(), reason) - }, - } - - // Plan-level Continue: independent tasks keep running when one fails. - plan := orchestrator.NewPlan( - client, - orchestrator.WithHooks(hooks), - orchestrator.OnError(orchestrator.Continue), - ) - - // --- Task definitions --- - - // Level 0: health check (no deps, functional task) - checkHealth := plan.TaskFunc( - "check-health", - func( - ctx context.Context, - c *osapi.Client, - ) (*orchestrator.Result, error) { - resp, err := c.Health.Liveness(ctx) - if err != nil { - return nil, fmt.Errorf("health check: %w", err) - } - - if resp.StatusCode() != 200 { - return nil, fmt.Errorf( - "unhealthy: status %d", - resp.StatusCode(), - ) - } - - return &orchestrator.Result{Changed: false}, nil - }, - ) - - // Level 1: parallel queries (all depend on health) - getHostname := plan.Task("get-hostname", &orchestrator.Op{ - Operation: "node.hostname.get", - Target: "_any", - }) - getHostname.DependsOn(checkHealth) - - getDisk := plan.Task("get-disk", &orchestrator.Op{ - Operation: "node.disk.get", - Target: "_any", - }) - getDisk.DependsOn(checkHealth) - - getMemory := plan.Task("get-memory", &orchestrator.Op{ - Operation: "node.memory.get", - Target: "_any", - }) - getMemory.DependsOn(checkHealth) - - getLoad := plan.Task("get-load", &orchestrator.Op{ - Operation: "node.load.get", - Target: "_any", - }) - getLoad.DependsOn(checkHealth) - getLoad.OnError(orchestrator.Retry(2)) // retry up to 2 times on failure - - // Level 1: Op with params — run uptime command via command.exec - runUptime := plan.Task("run-uptime", &orchestrator.Op{ - Operation: "command.exec.execute", - Target: "_any", - Params: map[string]any{ - "command": "uptime", - "args": []string{"-s"}, - }, - }) - runUptime.DependsOn(checkHealth) - - // Level 1: independent task that intentionally fails — demonstrates - // Continue strategy allowing other tasks to proceed. - optionalFail := plan.TaskFunc( - "optional-fail", - func( - _ context.Context, - _ *osapi.Client, - ) (*orchestrator.Result, error) { - return nil, fmt.Errorf("intentional failure to demonstrate Continue strategy") - }, - ) - optionalFail.OnError(orchestrator.Continue) - - // Level 2: summary — uses TaskFuncWithResults to read data from prior - // tasks and aggregate it. This is the key inter-task data passing pattern. - summary := plan.TaskFuncWithResults( - "print-summary", - func( - _ context.Context, - _ *osapi.Client, - results orchestrator.Results, - ) (*orchestrator.Result, error) { - fmt.Println("\n --- Fleet Summary ---") - - // Read hostname from a prior task via Results.Get(). - if r := results.Get("get-hostname"); r != nil { - if h, ok := r.Data["hostname"].(string); ok { - fmt.Printf(" Hostname: %s\n", h) - } - } - - // Read uptime stdout from a prior command task. - if r := results.Get("run-uptime"); r != nil { - if stdout, ok := r.Data["stdout"].(string); ok { - fmt.Printf(" Uptime: %s\n", stdout) - } - } - - // Return aggregated data — available in Report.Tasks[].Data - // after plan execution completes. - return &orchestrator.Result{ - Changed: false, - Data: map[string]any{"completed": true}, - }, nil - }, - ) - summary.DependsOn(getHostname, getDisk, getMemory, getLoad, runUptime) - summary.OnlyIfChanged() // skip if no dependency reported changes - - // Guard using Status inspection — only run if hostname succeeded. - summary.When(func(results orchestrator.Results) bool { - r := results.Get("get-hostname") - - return r != nil && r.Status == orchestrator.StatusChanged - }) - - // Level 2: alert task — runs only if optional-fail has StatusFailed. - // Demonstrates using Status in a When guard for failure-triggered recovery. - alertOnFailure := plan.TaskFunc( - "alert-on-failure", - func( - _ context.Context, - _ *osapi.Client, - ) (*orchestrator.Result, error) { - fmt.Println("\n [alert] optional-fail task failed — sending alert") - - return &orchestrator.Result{ - Changed: true, - Data: map[string]any{"alerted": true}, - }, nil - }, - ) - alertOnFailure.DependsOn(optionalFail) - alertOnFailure.When(func(results orchestrator.Results) bool { - r := results.Get("optional-fail") - - return r != nil && r.Status == orchestrator.StatusFailed - }) - - // --- Structured DAG access --- - - levels, err := plan.Levels() - if err != nil { - log.Fatalf("invalid plan: %v", err) - } - - fmt.Printf( - "DAG: %d tasks across %d levels\n\n", - len(plan.Tasks()), - len(levels), - ) - - for i, level := range levels { - names := make([]string, len(level)) - for j, t := range level { - names[j] = t.Name() - } - - fmt.Printf(" Level %d: %s\n", i, strings.Join(names, ", ")) - } - - fmt.Println() - - // --- Run --- - - report, err := plan.Run(context.Background()) - if err != nil { - log.Fatalf("plan failed: %v", err) - } - - // --- Detailed result inspection --- - // TaskResult.Data carries operation response data for post-run access. - - fmt.Println("\nDetailed results:") - - for _, r := range report.Tasks { - status := string(r.Status) - if r.Error != nil { - status += fmt.Sprintf(" (%s)", r.Error) - } - - fmt.Printf( - " %-20s status=%-12s changed=%-5v duration=%s\n", - r.Name, - status, - r.Changed, - r.Duration, - ) - - if len(r.Data) > 0 { - b, _ := json.MarshalIndent(r.Data, " "+strings.Repeat(" ", 20), " ") - fmt.Printf(" %-20s data=%s\n", "", b) - } - } -} diff --git a/examples/discovery/main.go b/examples/discovery/main.go deleted file mode 100644 index b1f783c..0000000 --- a/examples/discovery/main.go +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) 2026 John Dewey - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -// Package main demonstrates a runnable orchestrator plan that discovers -// fleet information from a live OSAPI instance. -// -// DAG: -// -// check-health -// ├── list-agents ───────┐ -// ├── get-status ───────┤ -// ├── get-load ───┐ │ -// └── get-memory ───┴───┴── print-summary (when: agents found) -// -// Run with: OSAPI_TOKEN="" go run main.go -package main - -import ( - "context" - "fmt" - "log" - "net/http" - "os" - "strings" - - "github.com/osapi-io/osapi-sdk/pkg/orchestrator" - "github.com/osapi-io/osapi-sdk/pkg/osapi" -) - -func main() { - url := os.Getenv("OSAPI_URL") - if url == "" { - url = "http://localhost:8080" - } - - token := os.Getenv("OSAPI_TOKEN") - if token == "" { - log.Fatal("OSAPI_TOKEN environment variable is required") - } - - client := osapi.New(url, token) - - hooks := orchestrator.Hooks{ - BeforePlan: func(summary orchestrator.PlanSummary) { - fmt.Println("=== Execution Plan ===") - fmt.Printf("Plan: %d tasks, %d steps\n", summary.TotalTasks, len(summary.Steps)) - fmt.Println() - }, - AfterPlan: func(report *orchestrator.Report) { - fmt.Printf( - "\n=== Complete: %s in %s ===\n", - report.Summary(), - report.Duration, - ) - }, - BeforeLevel: func( - level int, - tasks []*orchestrator.Task, - parallel bool, - ) { - names := make([]string, len(tasks)) - for i, t := range tasks { - names[i] = t.Name() - } - - mode := "sequential" - if parallel { - mode = "parallel" - } - - fmt.Printf( - "\n>>> Step %d (%s): %s\n", - level+1, - mode, - strings.Join(names, ", "), - ) - }, - AfterLevel: func(level int, results []orchestrator.TaskResult) { - changed := 0 - for _, r := range results { - if r.Changed { - changed++ - } - } - - fmt.Printf( - "<<< Step %d done: %d/%d changed\n", - level+1, - changed, - len(results), - ) - }, - BeforeTask: func(task *orchestrator.Task) { - fmt.Printf(" [start] %s (custom function)\n", task.Name()) - }, - AfterTask: func( - _ *orchestrator.Task, - result orchestrator.TaskResult, - ) { - fmt.Printf( - " [%s] %s changed=%v duration=%s\n", - result.Status, - result.Name, - result.Changed, - result.Duration, - ) - }, - OnRetry: func( - task *orchestrator.Task, - attempt int, - err error, - ) { - fmt.Printf( - " [retry] %s attempt=%d error=%q\n", - task.Name(), - attempt, - err, - ) - }, - OnSkip: func(task *orchestrator.Task, reason string) { - fmt.Printf(" [skip] %s reason=%q\n", task.Name(), reason) - }, - } - - plan := orchestrator.NewPlan(client, orchestrator.WithHooks(hooks)) - - // Level 0: verify OSAPI is healthy before doing anything. - checkHealth := plan.TaskFunc("check-health", func( - ctx context.Context, - c *osapi.Client, - ) (*orchestrator.Result, error) { - resp, err := c.Health.Liveness(ctx) - if err != nil { - return nil, fmt.Errorf("health check: %w", err) - } - - if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf( - "API not healthy: %d", - resp.StatusCode(), - ) - } - - return &orchestrator.Result{Changed: false}, nil - }) - - // Level 1: discover agents (parallel with others). - listAgents := plan.TaskFunc("list-agents", func( - ctx context.Context, - c *osapi.Client, - ) (*orchestrator.Result, error) { - resp, err := c.Agent.List(ctx) - if err != nil { - return nil, fmt.Errorf("list agents: %w", err) - } - - if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf( - "list agents: %d", - resp.StatusCode(), - ) - } - - agents := resp.JSON200 - - hostnames := make([]string, len(agents.Agents)) - for i, a := range agents.Agents { - hostnames[i] = a.Hostname - } - - return &orchestrator.Result{ - Changed: false, - Data: map[string]any{ - "total": agents.Total, - "hostnames": hostnames, - }, - }, nil - }) - - // Level 1: get system status (parallel with list-agents). - getStatus := plan.TaskFunc("get-status", func( - ctx context.Context, - c *osapi.Client, - ) (*orchestrator.Result, error) { - resp, err := c.Health.Status(ctx) - if err != nil { - return nil, fmt.Errorf("get status: %w", err) - } - - if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf( - "get status: %d", - resp.StatusCode(), - ) - } - - s := resp.JSON200 - - return &orchestrator.Result{ - Changed: false, - Data: map[string]any{ - "status": s.Status, - "version": s.Version, - "uptime": s.Uptime, - }, - }, nil - }) - - // Level 1: get load averages (parallel with list-agents). - getLoad := plan.TaskFunc("get-load", func( - ctx context.Context, - c *osapi.Client, - ) (*orchestrator.Result, error) { - resp, err := c.Node.Load(ctx, "_any") - if err != nil { - return nil, fmt.Errorf("get load: %w", err) - } - - if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf( - "get load: %d", - resp.StatusCode(), - ) - } - - return &orchestrator.Result{Changed: false}, nil - }) - - // Level 1: get memory info (parallel with list-agents). - getMemory := plan.TaskFunc("get-memory", func( - ctx context.Context, - c *osapi.Client, - ) (*orchestrator.Result, error) { - resp, err := c.Node.Memory(ctx, "_any") - if err != nil { - return nil, fmt.Errorf("get memory: %w", err) - } - - if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf( - "get memory: %d", - resp.StatusCode(), - ) - } - - return &orchestrator.Result{Changed: false}, nil - }) - - // Level 2: print summary — only if agents were found. - summary := plan.TaskFunc("print-summary", func( - _ context.Context, - _ *osapi.Client, - ) (*orchestrator.Result, error) { - return &orchestrator.Result{Changed: false}, nil - }) - - // Wire dependencies. - listAgents.DependsOn(checkHealth) - getStatus.DependsOn(checkHealth) - getLoad.DependsOn(checkHealth) - getMemory.DependsOn(checkHealth) - summary.DependsOn(listAgents, getStatus, getLoad, getMemory) - - // Only print summary if at least one agent was found. - summary.When(func(results orchestrator.Results) bool { - r := results.Get("list-agents") - if r == nil { - return false - } - - total, _ := r.Data["total"].(int) - - return total > 0 - }) - - // Run the plan. - _, err := plan.Run(context.Background()) - if err != nil { - log.Fatal(err) - } -} diff --git a/examples/orchestration/basic/go.mod b/examples/orchestration/basic/go.mod new file mode 100644 index 0000000..966cb20 --- /dev/null +++ b/examples/orchestration/basic/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/basic + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/all/go.sum b/examples/orchestration/basic/go.sum similarity index 100% rename from examples/all/go.sum rename to examples/orchestration/basic/go.sum diff --git a/examples/orchestration/basic/main.go b/examples/orchestration/basic/main.go new file mode 100644 index 0000000..ae22a3c --- /dev/null +++ b/examples/orchestration/basic/main.go @@ -0,0 +1,91 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates the simplest orchestrator plan: a health +// check followed by a hostname query using Op tasks with DependsOn. +// +// DAG: +// +// check-health +// └── get-hostname +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + BeforeTask: func(task *orchestrator.Task) { + fmt.Printf(" [start] %s\n", task.Name()) + }, + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf(" [%s] %s changed=%v\n", + result.Status, result.Name, result.Changed) + }, + } + + plan := orchestrator.NewPlan(client, orchestrator.WithHooks(hooks)) + + health := plan.TaskFunc( + "check-health", + func(ctx context.Context, c *osapi.Client) (*orchestrator.Result, error) { + _, err := c.Health.Liveness(ctx) + if err != nil { + return nil, fmt.Errorf("health: %w", err) + } + + return &orchestrator.Result{Changed: false}, nil + }, + ) + + hostname := plan.Task("get-hostname", &orchestrator.Op{ + Operation: "node.hostname.get", + Target: "_any", + }) + hostname.DependsOn(health) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/orchestration/broadcast/go.mod b/examples/orchestration/broadcast/go.mod new file mode 100644 index 0000000..e28f1fc --- /dev/null +++ b/examples/orchestration/broadcast/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/broadcast + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/discovery/go.sum b/examples/orchestration/broadcast/go.sum similarity index 100% rename from examples/discovery/go.sum rename to examples/orchestration/broadcast/go.sum diff --git a/examples/orchestration/broadcast/main.go b/examples/orchestration/broadcast/main.go new file mode 100644 index 0000000..605e18b --- /dev/null +++ b/examples/orchestration/broadcast/main.go @@ -0,0 +1,106 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates broadcast targeting with _all. The +// operation is sent to every registered agent and per-host results +// are available via HostResults. +// +// DAG: +// +// get-hostname-all (_all broadcast) +// └── print-hosts (reads HostResults) +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf(" [%s] %s changed=%v\n", + result.Status, result.Name, result.Changed) + + // Show per-host results for broadcast operations. + for _, hr := range result.HostResults { + fmt.Printf(" host=%s changed=%v\n", + hr.Hostname, hr.Changed) + } + }, + } + + plan := orchestrator.NewPlan(client, orchestrator.WithHooks(hooks)) + + // Target _all: delivered to every registered agent. + getAll := plan.Task("get-hostname-all", &orchestrator.Op{ + Operation: "node.hostname.get", + Target: "_all", + }) + + // Access per-host results from broadcast tasks. + printHosts := plan.TaskFuncWithResults( + "print-hosts", + func( + _ context.Context, + _ *osapi.Client, + results orchestrator.Results, + ) (*orchestrator.Result, error) { + r := results.Get("get-hostname-all") + if r == nil { + return &orchestrator.Result{Changed: false}, nil + } + + fmt.Printf("\n Hosts responded: %d\n", len(r.HostResults)) + for _, hr := range r.HostResults { + fmt.Printf(" %s\n", hr.Hostname) + } + + return &orchestrator.Result{Changed: false}, nil + }, + ) + printHosts.DependsOn(getAll) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/orchestration/error-strategy/go.mod b/examples/orchestration/error-strategy/go.mod new file mode 100644 index 0000000..45aea6b --- /dev/null +++ b/examples/orchestration/error-strategy/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/error-strategy + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/orchestration/error-strategy/go.sum b/examples/orchestration/error-strategy/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/orchestration/error-strategy/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/orchestration/error-strategy/main.go b/examples/orchestration/error-strategy/main.go new file mode 100644 index 0000000..f17408a --- /dev/null +++ b/examples/orchestration/error-strategy/main.go @@ -0,0 +1,95 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates error strategies: Continue vs StopAll. +// +// With Continue, independent tasks keep running when one fails. +// With StopAll (default), the entire plan halts on the first failure. +// +// DAG: +// +// might-fail (continue) +// get-hostname (independent, runs despite failure) +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + status := string(result.Status) + if result.Error != nil { + status += fmt.Sprintf(" (%s)", result.Error) + } + + fmt.Printf(" [%s] %s\n", status, result.Name) + }, + } + + // Plan-level Continue: don't halt on failure. + plan := orchestrator.NewPlan( + client, + orchestrator.WithHooks(hooks), + orchestrator.OnError(orchestrator.Continue), + ) + + // This task fails, but Continue lets the plan proceed. + plan.TaskFunc( + "might-fail", + func(_ context.Context, _ *osapi.Client) (*orchestrator.Result, error) { + return nil, fmt.Errorf("simulated failure") + }, + ) + + // Independent task — runs despite the failure above. + plan.Task("get-hostname", &orchestrator.Op{ + Operation: "node.hostname.get", + Target: "_any", + }) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/orchestration/guards/go.mod b/examples/orchestration/guards/go.mod new file mode 100644 index 0000000..2f6228a --- /dev/null +++ b/examples/orchestration/guards/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/guards + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/orchestration/guards/go.sum b/examples/orchestration/guards/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/orchestration/guards/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/orchestration/guards/main.go b/examples/orchestration/guards/main.go new file mode 100644 index 0000000..45c12af --- /dev/null +++ b/examples/orchestration/guards/main.go @@ -0,0 +1,109 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates When() guard predicates for conditional +// task execution. The summary task only runs if the hostname step +// succeeded. +// +// DAG: +// +// check-health +// └── get-hostname +// └── print-summary (when: hostname changed) +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf(" [%s] %s\n", result.Status, result.Name) + }, + OnSkip: func(task *orchestrator.Task, reason string) { + fmt.Printf(" [skip] %s reason=%q\n", task.Name(), reason) + }, + } + + plan := orchestrator.NewPlan(client, orchestrator.WithHooks(hooks)) + + health := plan.TaskFunc( + "check-health", + func(ctx context.Context, c *osapi.Client) (*orchestrator.Result, error) { + _, err := c.Health.Liveness(ctx) + if err != nil { + return nil, fmt.Errorf("health: %w", err) + } + + return &orchestrator.Result{Changed: false}, nil + }, + ) + + getHostname := plan.Task("get-hostname", &orchestrator.Op{ + Operation: "node.hostname.get", + Target: "_any", + }) + getHostname.DependsOn(health) + + summary := plan.TaskFunc( + "print-summary", + func(_ context.Context, _ *osapi.Client) (*orchestrator.Result, error) { + fmt.Println("\n Hostname was retrieved successfully!") + + return &orchestrator.Result{Changed: false}, nil + }, + ) + summary.DependsOn(getHostname) + + // Guard: only run if get-hostname reported StatusChanged. + summary.When(func(results orchestrator.Results) bool { + r := results.Get("get-hostname") + + return r != nil && r.Status == orchestrator.StatusChanged + }) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/orchestration/hooks/go.mod b/examples/orchestration/hooks/go.mod new file mode 100644 index 0000000..49af8cd --- /dev/null +++ b/examples/orchestration/hooks/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/hooks + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/orchestration/hooks/go.sum b/examples/orchestration/hooks/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/orchestration/hooks/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/orchestration/hooks/main.go b/examples/orchestration/hooks/main.go new file mode 100644 index 0000000..f706ab7 --- /dev/null +++ b/examples/orchestration/hooks/main.go @@ -0,0 +1,143 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates all 8 lifecycle hooks: BeforePlan, +// AfterPlan, BeforeLevel, AfterLevel, BeforeTask, AfterTask, +// OnRetry, and OnSkip. +// +// DAG: +// +// check-health +// ├── get-hostname +// └── get-disk +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "strings" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + BeforePlan: func(summary orchestrator.PlanSummary) { + fmt.Printf("=== Plan: %d tasks, %d steps ===\n", + summary.TotalTasks, len(summary.Steps)) + }, + AfterPlan: func(report *orchestrator.Report) { + fmt.Printf("\n=== Done: %s in %s ===\n", + report.Summary(), report.Duration) + }, + BeforeLevel: func(level int, tasks []*orchestrator.Task, parallel bool) { + names := make([]string, len(tasks)) + for i, t := range tasks { + names[i] = t.Name() + } + + mode := "sequential" + if parallel { + mode = "parallel" + } + + fmt.Printf("\n>>> Step %d (%s): %s\n", + level+1, mode, strings.Join(names, ", ")) + }, + AfterLevel: func(level int, results []orchestrator.TaskResult) { + changed := 0 + for _, r := range results { + if r.Changed { + changed++ + } + } + + fmt.Printf("<<< Step %d: %d/%d changed\n", + level+1, changed, len(results)) + }, + BeforeTask: func(task *orchestrator.Task) { + if op := task.Operation(); op != nil { + fmt.Printf(" [start] %s op=%s\n", + task.Name(), op.Operation) + } else { + fmt.Printf(" [start] %s (func)\n", task.Name()) + } + }, + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf(" [%s] %s changed=%v duration=%s\n", + result.Status, result.Name, result.Changed, result.Duration) + }, + OnRetry: func(task *orchestrator.Task, attempt int, err error) { + fmt.Printf(" [retry] %s attempt=%d err=%q\n", + task.Name(), attempt, err) + }, + OnSkip: func(task *orchestrator.Task, reason string) { + fmt.Printf(" [skip] %s reason=%q\n", task.Name(), reason) + }, + } + + plan := orchestrator.NewPlan(client, orchestrator.WithHooks(hooks)) + + health := plan.TaskFunc( + "check-health", + func(ctx context.Context, c *osapi.Client) (*orchestrator.Result, error) { + _, err := c.Health.Liveness(ctx) + if err != nil { + return nil, fmt.Errorf("health: %w", err) + } + + return &orchestrator.Result{Changed: false}, nil + }, + ) + + hostname := plan.Task("get-hostname", &orchestrator.Op{ + Operation: "node.hostname.get", + Target: "_any", + }) + hostname.DependsOn(health) + + disk := plan.Task("get-disk", &orchestrator.Op{ + Operation: "node.disk.get", + Target: "_any", + }) + disk.DependsOn(health) + + _, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/orchestration/only-if-changed/go.mod b/examples/orchestration/only-if-changed/go.mod new file mode 100644 index 0000000..ff85586 --- /dev/null +++ b/examples/orchestration/only-if-changed/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/only-if-changed + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/orchestration/only-if-changed/go.sum b/examples/orchestration/only-if-changed/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/orchestration/only-if-changed/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/orchestration/only-if-changed/main.go b/examples/orchestration/only-if-changed/main.go new file mode 100644 index 0000000..f2eed2e --- /dev/null +++ b/examples/orchestration/only-if-changed/main.go @@ -0,0 +1,89 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates OnlyIfChanged() — a task that is skipped +// unless at least one dependency reported a change. +// +// DAG: +// +// get-hostname +// └── log-change (only-if-changed) +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf(" [%s] %s changed=%v\n", + result.Status, result.Name, result.Changed) + }, + OnSkip: func(task *orchestrator.Task, reason string) { + fmt.Printf(" [skip] %s reason=%q\n", task.Name(), reason) + }, + } + + plan := orchestrator.NewPlan(client, orchestrator.WithHooks(hooks)) + + getHostname := plan.Task("get-hostname", &orchestrator.Op{ + Operation: "node.hostname.get", + Target: "_any", + }) + + logChange := plan.TaskFunc( + "log-change", + func(_ context.Context, _ *osapi.Client) (*orchestrator.Result, error) { + fmt.Println("\n Dependencies changed — logging event.") + + return &orchestrator.Result{Changed: true}, nil + }, + ) + logChange.DependsOn(getHostname) + logChange.OnlyIfChanged() + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/orchestration/only-if-failed/go.mod b/examples/orchestration/only-if-failed/go.mod new file mode 100644 index 0000000..d92f3b2 --- /dev/null +++ b/examples/orchestration/only-if-failed/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/only-if-failed + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/orchestration/only-if-failed/go.sum b/examples/orchestration/only-if-failed/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/orchestration/only-if-failed/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/orchestration/only-if-failed/main.go b/examples/orchestration/only-if-failed/main.go new file mode 100644 index 0000000..cd9b14a --- /dev/null +++ b/examples/orchestration/only-if-failed/main.go @@ -0,0 +1,107 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates failure-triggered recovery using a When +// guard that checks for StatusFailed. The alert task only runs when +// the upstream task has failed. +// +// DAG: +// +// might-fail (continue on error) +// └── alert (when: might-fail == StatusFailed) +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + status := string(result.Status) + if result.Error != nil { + status += fmt.Sprintf(" (%s)", result.Error) + } + + fmt.Printf(" [%s] %s\n", status, result.Name) + }, + OnSkip: func(task *orchestrator.Task, reason string) { + fmt.Printf(" [skip] %s reason=%q\n", task.Name(), reason) + }, + } + + plan := orchestrator.NewPlan( + client, + orchestrator.WithHooks(hooks), + orchestrator.OnError(orchestrator.Continue), + ) + + // A task that intentionally fails. + mightFail := plan.TaskFunc( + "might-fail", + func(_ context.Context, _ *osapi.Client) (*orchestrator.Result, error) { + return nil, fmt.Errorf("simulated failure") + }, + ) + mightFail.OnError(orchestrator.Continue) + + // Recovery task — only runs if upstream failed. + alert := plan.TaskFunc( + "alert", + func(_ context.Context, _ *osapi.Client) (*orchestrator.Result, error) { + fmt.Println("\n Upstream failed — sending alert!") + + return &orchestrator.Result{Changed: true}, nil + }, + ) + alert.DependsOn(mightFail) + alert.When(func(results orchestrator.Results) bool { + r := results.Get("might-fail") + + return r != nil && r.Status == orchestrator.StatusFailed + }) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/orchestration/parallel/go.mod b/examples/orchestration/parallel/go.mod new file mode 100644 index 0000000..66cb251 --- /dev/null +++ b/examples/orchestration/parallel/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/parallel + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/orchestration/parallel/go.sum b/examples/orchestration/parallel/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/orchestration/parallel/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/orchestration/parallel/main.go b/examples/orchestration/parallel/main.go new file mode 100644 index 0000000..8aec59e --- /dev/null +++ b/examples/orchestration/parallel/main.go @@ -0,0 +1,112 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates parallel task execution. Tasks at the same +// DAG level run concurrently. +// +// DAG: +// +// check-health +// ├── get-hostname +// ├── get-disk +// └── get-memory +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "strings" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + BeforeLevel: func(level int, tasks []*orchestrator.Task, parallel bool) { + names := make([]string, len(tasks)) + for i, t := range tasks { + names[i] = t.Name() + } + + mode := "sequential" + if parallel { + mode = "parallel" + } + + fmt.Printf("Step %d (%s): %s\n", level+1, mode, strings.Join(names, ", ")) + }, + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf(" [%s] %s duration=%s\n", + result.Status, result.Name, result.Duration) + }, + } + + plan := orchestrator.NewPlan(client, orchestrator.WithHooks(hooks)) + + health := plan.TaskFunc( + "check-health", + func(ctx context.Context, c *osapi.Client) (*orchestrator.Result, error) { + _, err := c.Health.Liveness(ctx) + if err != nil { + return nil, fmt.Errorf("health: %w", err) + } + + return &orchestrator.Result{Changed: false}, nil + }, + ) + + // Three tasks at the same level — all depend on health, + // so the engine runs them in parallel. + for _, op := range []struct{ name, operation string }{ + {"get-hostname", "node.hostname.get"}, + {"get-disk", "node.disk.get"}, + {"get-memory", "node.memory.get"}, + } { + t := plan.Task(op.name, &orchestrator.Op{ + Operation: op.operation, + Target: "_any", + }) + t.DependsOn(health) + } + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/orchestration/result-decode/go.mod b/examples/orchestration/result-decode/go.mod new file mode 100644 index 0000000..8be15ed --- /dev/null +++ b/examples/orchestration/result-decode/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/result-decode + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/orchestration/result-decode/go.sum b/examples/orchestration/result-decode/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/orchestration/result-decode/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/orchestration/result-decode/main.go b/examples/orchestration/result-decode/main.go new file mode 100644 index 0000000..5eb04a8 --- /dev/null +++ b/examples/orchestration/result-decode/main.go @@ -0,0 +1,79 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates post-execution result access via +// Report.Tasks[].Data. After the plan completes, task results +// can be inspected programmatically. +// +// DAG: +// +// get-hostname +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + plan := orchestrator.NewPlan(client) + + plan.Task("get-hostname", &orchestrator.Op{ + Operation: "node.hostname.get", + Target: "_any", + }) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%s in %s\n\n", report.Summary(), report.Duration) + + // Inspect task results after execution. + for _, r := range report.Tasks { + fmt.Printf("Task: %s status=%s changed=%v\n", + r.Name, r.Status, r.Changed) + + if len(r.Data) > 0 { + b, _ := json.MarshalIndent(r.Data, " ", " ") + fmt.Printf(" data=%s\n", b) + } + } +} diff --git a/examples/orchestration/retry/go.mod b/examples/orchestration/retry/go.mod new file mode 100644 index 0000000..e99cae3 --- /dev/null +++ b/examples/orchestration/retry/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/retry + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/orchestration/retry/go.sum b/examples/orchestration/retry/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/orchestration/retry/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/orchestration/retry/main.go b/examples/orchestration/retry/main.go new file mode 100644 index 0000000..eb15b6c --- /dev/null +++ b/examples/orchestration/retry/main.go @@ -0,0 +1,77 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates Retry(n) for automatic retry on failure. +// +// DAG: +// +// get-load [retry:3] +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf(" [%s] %s\n", result.Status, result.Name) + }, + OnRetry: func(task *orchestrator.Task, attempt int, err error) { + fmt.Printf(" [retry] %s attempt=%d error=%q\n", + task.Name(), attempt, err) + }, + } + + plan := orchestrator.NewPlan(client, orchestrator.WithHooks(hooks)) + + getLoad := plan.Task("get-load", &orchestrator.Op{ + Operation: "node.load.get", + Target: "_any", + }) + getLoad.OnError(orchestrator.Retry(3)) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/orchestration/task-func-results/go.mod b/examples/orchestration/task-func-results/go.mod new file mode 100644 index 0000000..5b46548 --- /dev/null +++ b/examples/orchestration/task-func-results/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/task-func-results + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/orchestration/task-func-results/go.sum b/examples/orchestration/task-func-results/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/orchestration/task-func-results/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/orchestration/task-func-results/main.go b/examples/orchestration/task-func-results/main.go new file mode 100644 index 0000000..9f0609f --- /dev/null +++ b/examples/orchestration/task-func-results/main.go @@ -0,0 +1,108 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates TaskFuncWithResults for reading data from +// previously completed tasks. The summary step reads hostname data +// set by a prior Op task. +// +// DAG: +// +// check-health +// └── get-hostname +// └── print-summary (reads get-hostname data) +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf(" [%s] %s\n", result.Status, result.Name) + }, + } + + plan := orchestrator.NewPlan(client, orchestrator.WithHooks(hooks)) + + health := plan.TaskFunc( + "check-health", + func(ctx context.Context, c *osapi.Client) (*orchestrator.Result, error) { + _, err := c.Health.Liveness(ctx) + if err != nil { + return nil, fmt.Errorf("health: %w", err) + } + + return &orchestrator.Result{Changed: false}, nil + }, + ) + + getHostname := plan.Task("get-hostname", &orchestrator.Op{ + Operation: "node.hostname.get", + Target: "_any", + }) + getHostname.DependsOn(health) + + // TaskFuncWithResults: access completed task data via Results.Get(). + summary := plan.TaskFuncWithResults( + "print-summary", + func( + _ context.Context, + _ *osapi.Client, + results orchestrator.Results, + ) (*orchestrator.Result, error) { + if r := results.Get("get-hostname"); r != nil { + if h, ok := r.Data["hostname"].(string); ok { + fmt.Printf("\n Hostname: %s\n", h) + } + } + + return &orchestrator.Result{Changed: false}, nil + }, + ) + summary.DependsOn(getHostname) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/orchestration/task-func/go.mod b/examples/orchestration/task-func/go.mod new file mode 100644 index 0000000..a5a9a24 --- /dev/null +++ b/examples/orchestration/task-func/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/orchestration/task-func + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/orchestration/task-func/go.sum b/examples/orchestration/task-func/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/orchestration/task-func/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/orchestration/task-func/main.go b/examples/orchestration/task-func/main.go new file mode 100644 index 0000000..263bbaf --- /dev/null +++ b/examples/orchestration/task-func/main.go @@ -0,0 +1,92 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates TaskFunc for embedding custom Go logic +// in an orchestration plan. +// +// DAG: +// +// check-health (TaskFunc) +// └── get-hostname (Op) +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/orchestrator" + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + + hooks := orchestrator.Hooks{ + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf(" [%s] %s changed=%v\n", + result.Status, result.Name, result.Changed) + }, + } + + plan := orchestrator.NewPlan(client, orchestrator.WithHooks(hooks)) + + // TaskFunc: run arbitrary Go code as a plan step. + health := plan.TaskFunc( + "check-health", + func( + ctx context.Context, + c *osapi.Client, + ) (*orchestrator.Result, error) { + _, err := c.Health.Liveness(ctx) + if err != nil { + return nil, fmt.Errorf("health check: %w", err) + } + + return &orchestrator.Result{Changed: false}, nil + }, + ) + + hostname := plan.Task("get-hostname", &orchestrator.Op{ + Operation: "node.hostname.get", + Target: "_any", + }) + hostname.DependsOn(health) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/osapi/agent/go.mod b/examples/osapi/agent/go.mod new file mode 100644 index 0000000..2036e61 --- /dev/null +++ b/examples/osapi/agent/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/osapi/agent + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/osapi/agent/go.sum b/examples/osapi/agent/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/osapi/agent/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/osapi/agent/main.go b/examples/osapi/agent/main.go new file mode 100644 index 0000000..68a17b3 --- /dev/null +++ b/examples/osapi/agent/main.go @@ -0,0 +1,114 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates the AgentService: listing the fleet and +// retrieving rich facts for a specific agent — OS info, load averages, +// memory stats, network interfaces, labels, and lifecycle timestamps. +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + ctx := context.Background() + + // List all active agents. + list, err := client.Agent.List(ctx) + if err != nil { + log.Fatalf("list agents: %v", err) + } + + fmt.Printf("Agents: %d total\n", list.Data.Total) + + for _, a := range list.Data.Agents { + fmt.Printf(" %s status=%s labels=%v\n", + a.Hostname, a.Status, a.Labels) + } + + if len(list.Data.Agents) == 0 { + return + } + + // Get rich facts for the first agent. + hostname := list.Data.Agents[0].Hostname + + resp, err := client.Agent.Get(ctx, hostname) + if err != nil { + log.Fatalf("get agent %s: %v", hostname, err) + } + + a := resp.Data + + fmt.Printf("\nAgent: %s\n", a.Hostname) + fmt.Printf(" Status: %s\n", a.Status) + fmt.Printf(" Architecture: %s\n", a.Architecture) + fmt.Printf(" Kernel: %s\n", a.KernelVersion) + fmt.Printf(" CPUs: %d\n", a.CPUCount) + fmt.Printf(" FQDN: %s\n", a.Fqdn) + fmt.Printf(" Package Mgr: %s\n", a.PackageMgr) + fmt.Printf(" Service Mgr: %s\n", a.ServiceMgr) + fmt.Printf(" Uptime: %s\n", a.Uptime) + fmt.Printf(" Started: %s\n", a.StartedAt.Format("2006-01-02 15:04:05")) + fmt.Printf(" Registered: %s\n", a.RegisteredAt.Format("2006-01-02 15:04:05")) + + if a.OSInfo != nil { + fmt.Printf(" OS: %s %s\n", + a.OSInfo.Distribution, a.OSInfo.Version) + } + + if a.LoadAverage != nil { + fmt.Printf(" Load: %.2f %.2f %.2f\n", + a.LoadAverage.OneMin, + a.LoadAverage.FiveMin, + a.LoadAverage.FifteenMin) + } + + if a.Memory != nil { + fmt.Printf(" Memory: total=%d used=%d free=%d\n", + a.Memory.Total, a.Memory.Used, a.Memory.Free) + } + + if len(a.Interfaces) > 0 { + fmt.Printf(" Interfaces:\n") + for _, iface := range a.Interfaces { + fmt.Printf(" %-12s ipv4=%-15s mac=%s\n", + iface.Name, iface.IPv4, iface.MAC) + } + } +} diff --git a/examples/osapi/audit/go.mod b/examples/osapi/audit/go.mod new file mode 100644 index 0000000..cf005f3 --- /dev/null +++ b/examples/osapi/audit/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/osapi/audit + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/osapi/audit/go.sum b/examples/osapi/audit/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/osapi/audit/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/osapi/audit/main.go b/examples/osapi/audit/main.go new file mode 100644 index 0000000..91138ad --- /dev/null +++ b/examples/osapi/audit/main.go @@ -0,0 +1,80 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates the AuditService: listing audit entries, +// retrieving a specific entry, and exporting all entries. +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + ctx := context.Background() + + // List recent audit entries. + list, err := client.Audit.List(ctx, 10, 0) + if err != nil { + log.Fatalf("list audit: %v", err) + } + + fmt.Printf("Audit entries: %d total\n", list.Data.TotalItems) + + for _, e := range list.Data.Items { + fmt.Printf(" %s %s %s code=%d user=%s\n", + e.ID, e.Method, e.Path, e.ResponseCode, e.User) + } + + if len(list.Data.Items) == 0 { + return + } + + // Get a specific audit entry. + id := list.Data.Items[0].ID + + entry, err := client.Audit.Get(ctx, id) + if err != nil { + log.Fatalf("get audit %s: %v", id, err) + } + + fmt.Printf("\nEntry %s:\n", entry.Data.ID) + fmt.Printf(" Method: %s\n", entry.Data.Method) + fmt.Printf(" Path: %s\n", entry.Data.Path) + fmt.Printf(" User: %s\n", entry.Data.User) + fmt.Printf(" Duration: %dms\n", entry.Data.DurationMs) +} diff --git a/examples/osapi/command/go.mod b/examples/osapi/command/go.mod new file mode 100644 index 0000000..30e1a84 --- /dev/null +++ b/examples/osapi/command/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/osapi/command + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/osapi/command/go.sum b/examples/osapi/command/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/osapi/command/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/osapi/command/main.go b/examples/osapi/command/main.go new file mode 100644 index 0000000..e7570a8 --- /dev/null +++ b/examples/osapi/command/main.go @@ -0,0 +1,80 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates command execution: direct exec and +// shell-interpreted commands. +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + ctx := context.Background() + target := "_any" + + // Direct exec — runs a binary with arguments. + exec, err := client.Node.Exec(ctx, osapi.ExecRequest{ + Target: target, + Command: "uptime", + }) + if err != nil { + log.Fatalf("exec: %v", err) + } + + for _, r := range exec.Data.Results { + fmt.Printf("Exec (%s):\n", r.Hostname) + fmt.Printf(" stdout: %s\n", r.Stdout) + fmt.Printf(" exit: %d\n", r.ExitCode) + } + + // Shell — interpreted by /bin/sh, supports pipes and redirection. + shell, err := client.Node.Shell(ctx, osapi.ShellRequest{ + Target: target, + Command: "uname -a", + }) + if err != nil { + log.Fatalf("shell: %v", err) + } + + for _, r := range shell.Data.Results { + fmt.Printf("Shell (%s):\n", r.Hostname) + fmt.Printf(" stdout: %s\n", r.Stdout) + fmt.Printf(" exit: %d\n", r.ExitCode) + } +} diff --git a/examples/osapi/health/go.mod b/examples/osapi/health/go.mod new file mode 100644 index 0000000..70b9099 --- /dev/null +++ b/examples/osapi/health/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/osapi/health + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/osapi/health/go.sum b/examples/osapi/health/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/osapi/health/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/osapi/health/main.go b/examples/osapi/health/main.go new file mode 100644 index 0000000..e4e9ed2 --- /dev/null +++ b/examples/osapi/health/main.go @@ -0,0 +1,75 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates the HealthService: liveness, readiness, +// and detailed system status checks. +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + ctx := context.Background() + + // Liveness — is the API process running? + live, err := client.Health.Liveness(ctx) + if err != nil { + log.Fatalf("liveness: %v", err) + } + + fmt.Printf("Liveness: %s\n", live.Data.Status) + + // Readiness — is the API ready to serve requests? + ready, err := client.Health.Ready(ctx) + if err != nil { + log.Fatalf("readiness: %v", err) + } + + fmt.Printf("Readiness: %s\n", ready.Data.Status) + + // Status — detailed system info (requires auth). + status, err := client.Health.Status(ctx) + if err != nil { + log.Fatalf("status: %v", err) + } + + fmt.Printf("Status: %s\n", status.Data.Status) + fmt.Printf("Version: %s\n", status.Data.Version) + fmt.Printf("Uptime: %s\n", status.Data.Uptime) +} diff --git a/examples/all/go.mod b/examples/osapi/job/go.mod similarity index 84% rename from examples/all/go.mod rename to examples/osapi/job/go.mod index c819126..e407eca 100644 --- a/examples/all/go.mod +++ b/examples/osapi/job/go.mod @@ -1,8 +1,8 @@ -module github.com/osapi-io/osapi-sdk/examples/all +module github.com/osapi-io/osapi-sdk/examples/osapi/job go 1.25.7 -replace github.com/osapi-io/osapi-sdk => ../.. +replace github.com/osapi-io/osapi-sdk => ../../.. require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 diff --git a/examples/osapi/job/go.sum b/examples/osapi/job/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/osapi/job/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/osapi/job/main.go b/examples/osapi/job/main.go new file mode 100644 index 0000000..52dbb06 --- /dev/null +++ b/examples/osapi/job/main.go @@ -0,0 +1,92 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates the JobService: creating a job, polling +// for its result, listing jobs, and checking queue statistics. +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + ctx := context.Background() + + // Create a job. + created, err := client.Job.Create(ctx, map[string]any{ + "type": "node.hostname.get", + }, "_any") + if err != nil { + log.Fatalf("create job: %v", err) + } + + fmt.Printf("Created job: %s status=%s\n", + created.Data.JobID, created.Data.Status) + + // Poll until the job completes. + time.Sleep(2 * time.Second) + + job, err := client.Job.Get(ctx, created.Data.JobID) + if err != nil { + log.Fatalf("get job: %v", err) + } + + fmt.Printf("Job %s: status=%s\n", job.Data.ID, job.Data.Status) + + // List recent jobs. + list, err := client.Job.List(ctx, osapi.ListParams{Limit: 5}) + if err != nil { + log.Fatalf("list jobs: %v", err) + } + + fmt.Printf("\nRecent jobs: %d total\n", list.Data.TotalItems) + + for _, j := range list.Data.Items { + fmt.Printf(" %s status=%s op=%v\n", + j.ID, j.Status, j.Operation) + } + + // Queue statistics. + stats, err := client.Job.QueueStats(ctx) + if err != nil { + log.Fatalf("queue stats: %v", err) + } + + fmt.Printf("\nQueue: %d total jobs\n", stats.Data.TotalJobs) +} diff --git a/examples/osapi/metrics/go.mod b/examples/osapi/metrics/go.mod new file mode 100644 index 0000000..db5418a --- /dev/null +++ b/examples/osapi/metrics/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/osapi/metrics + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/osapi/metrics/go.sum b/examples/osapi/metrics/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/osapi/metrics/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/osapi/metrics/main.go b/examples/osapi/metrics/main.go new file mode 100644 index 0000000..3219036 --- /dev/null +++ b/examples/osapi/metrics/main.go @@ -0,0 +1,56 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates the MetricsService: fetching raw +// Prometheus metrics text from the /metrics endpoint. +// +// Run with: go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + ctx := context.Background() + + text, err := client.Metrics.Get(ctx) + if err != nil { + log.Fatalf("metrics: %v", err) + } + + fmt.Println(text) +} diff --git a/examples/osapi/network/go.mod b/examples/osapi/network/go.mod new file mode 100644 index 0000000..57b0bec --- /dev/null +++ b/examples/osapi/network/go.mod @@ -0,0 +1,20 @@ +module github.com/osapi-io/osapi-sdk/examples/osapi/network + +go 1.25.7 + +replace github.com/osapi-io/osapi-sdk => ../../.. + +require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect +) diff --git a/examples/osapi/network/go.sum b/examples/osapi/network/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/osapi/network/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/osapi/network/main.go b/examples/osapi/network/main.go new file mode 100644 index 0000000..c966677 --- /dev/null +++ b/examples/osapi/network/main.go @@ -0,0 +1,79 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates network operations: reading DNS config +// and running a ping check. +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + iface := os.Getenv("OSAPI_INTERFACE") + if iface == "" { + iface = "eth0" + } + + client := osapi.New(url, token) + ctx := context.Background() + target := "_any" + + // Get DNS configuration for an interface. + dns, err := client.Node.GetDNS(ctx, target, iface) + if err != nil { + log.Fatalf("get dns: %v", err) + } + + for _, r := range dns.Data.Results { + fmt.Printf("DNS (%s):\n", r.Hostname) + fmt.Printf(" Servers: %v\n", r.Servers) + fmt.Printf(" Search: %v\n", r.SearchDomains) + } + + // Ping a host. + ping, err := client.Node.Ping(ctx, target, "8.8.8.8") + if err != nil { + log.Fatalf("ping: %v", err) + } + + for _, r := range ping.Data.Results { + fmt.Printf("Ping (%s):\n", r.Hostname) + fmt.Printf(" Sent=%d Received=%d Loss=%.1f%%\n", + r.PacketsSent, r.PacketsReceived, r.PacketLoss) + } +} diff --git a/examples/discovery/go.mod b/examples/osapi/node/go.mod similarity index 84% rename from examples/discovery/go.mod rename to examples/osapi/node/go.mod index 8502a72..95fe4f9 100644 --- a/examples/discovery/go.mod +++ b/examples/osapi/node/go.mod @@ -1,8 +1,8 @@ -module github.com/osapi-io/osapi-sdk/examples/discovery +module github.com/osapi-io/osapi-sdk/examples/osapi/node go 1.25.7 -replace github.com/osapi-io/osapi-sdk => ../.. +replace github.com/osapi-io/osapi-sdk => ../../.. require github.com/osapi-io/osapi-sdk v0.0.0-00010101000000-000000000000 diff --git a/examples/osapi/node/go.sum b/examples/osapi/node/go.sum new file mode 100644 index 0000000..c6dbaf5 --- /dev/null +++ b/examples/osapi/node/go.sum @@ -0,0 +1,39 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/osapi/node/main.go b/examples/osapi/node/main.go new file mode 100644 index 0000000..2d899cc --- /dev/null +++ b/examples/osapi/node/main.go @@ -0,0 +1,142 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates the NodeService: querying status, hostname, +// OS info, disk, memory, load averages, and uptime from a target node. +// +// Run with: OSAPI_TOKEN="" go run main.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + client := osapi.New(url, token) + ctx := context.Background() + target := "_any" + + // Status (aggregated node info). + status, err := client.Node.Status(ctx, target) + if err != nil { + log.Fatalf("status: %v", err) + } + + for _, r := range status.Data.Results { + fmt.Printf("Status (%s):\n", r.Hostname) + fmt.Printf(" Uptime: %s\n", r.Uptime) + + if r.OSInfo != nil { + fmt.Printf(" OS: %s %s\n", r.OSInfo.Distribution, r.OSInfo.Version) + } + + if r.LoadAverage != nil { + fmt.Printf(" Load: %.2f %.2f %.2f\n", + r.LoadAverage.OneMin, r.LoadAverage.FiveMin, r.LoadAverage.FifteenMin) + } + } + + // Hostname + hn, err := client.Node.Hostname(ctx, target) + if err != nil { + log.Fatalf("hostname: %v", err) + } + + for _, r := range hn.Data.Results { + fmt.Printf("Hostname: %s\n", r.Hostname) + } + + // Disk usage + disk, err := client.Node.Disk(ctx, target) + if err != nil { + log.Fatalf("disk: %v", err) + } + + for _, r := range disk.Data.Results { + fmt.Printf("Disk (%s):\n", r.Hostname) + for _, d := range r.Disks { + fmt.Printf(" %s total=%d used=%d free=%d\n", + d.Name, d.Total, d.Used, d.Free) + } + } + + // Memory + mem, err := client.Node.Memory(ctx, target) + if err != nil { + log.Fatalf("memory: %v", err) + } + + for _, r := range mem.Data.Results { + fmt.Printf("Memory (%s): total=%d free=%d\n", + r.Hostname, r.Memory.Total, r.Memory.Free) + } + + // Load averages + load, err := client.Node.Load(ctx, target) + if err != nil { + log.Fatalf("load: %v", err) + } + + for _, r := range load.Data.Results { + fmt.Printf("Load (%s): %.2f %.2f %.2f\n", + r.Hostname, + r.LoadAverage.OneMin, + r.LoadAverage.FiveMin, + r.LoadAverage.FifteenMin) + } + + // OS info + osInfo, err := client.Node.OS(ctx, target) + if err != nil { + log.Fatalf("os: %v", err) + } + + for _, r := range osInfo.Data.Results { + if r.OSInfo != nil { + fmt.Printf("OS (%s): %s %s\n", + r.Hostname, r.OSInfo.Distribution, r.OSInfo.Version) + } + } + + // Uptime + up, err := client.Node.Uptime(ctx, target) + if err != nil { + log.Fatalf("uptime: %v", err) + } + + for _, r := range up.Data.Results { + fmt.Printf("Uptime (%s): %s\n", r.Hostname, r.Uptime) + } +} diff --git a/pkg/orchestrator/plan_public_test.go b/pkg/orchestrator/plan_public_test.go index f3e155a..7bf0bd1 100644 --- a/pkg/orchestrator/plan_public_test.go +++ b/pkg/orchestrator/plan_public_test.go @@ -839,7 +839,7 @@ func (s *PlanPublicTestSuite) TestRunOpTaskErrors() { validateFunc: func(report *orchestrator.Report, err error) { s.Error(err) s.NotNil(report) - s.Contains(err.Error(), "unexpected status 200") + s.Contains(err.Error(), "nil response body") }, }, { diff --git a/pkg/orchestrator/runner.go b/pkg/orchestrator/runner.go index f030851..6818852 100644 --- a/pkg/orchestrator/runner.go +++ b/pkg/orchestrator/runner.go @@ -3,11 +3,8 @@ package orchestrator import ( "context" "fmt" - "net/http" "sync" "time" - - "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" ) // runner executes a validated plan. @@ -482,20 +479,7 @@ func (r *runner) executeOp( return nil, fmt.Errorf("create job: %w", err) } - if createResp.StatusCode() != http.StatusCreated { - return nil, fmt.Errorf( - "create job: %s", - messageFromResponse( - createResp.StatusCode(), - createResp.JSON400, - createResp.JSON401, - createResp.JSON403, - createResp.JSON500, - ), - ) - } - - jobID := createResp.JSON201.JobId.String() + jobID := createResp.Data.JobID result, err := r.pollJob(ctx, jobID) if err != nil { @@ -540,31 +524,13 @@ func (r *runner) pollJob( return nil, fmt.Errorf("poll job %s: %w", jobID, err) } - if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf( - "poll job %s: %s", - jobID, - messageFromResponse( - resp.StatusCode(), - resp.JSON400, - resp.JSON401, - resp.JSON403, - resp.JSON404, - resp.JSON500, - ), - ) - } + job := resp.Data - status := "" - if resp.JSON200.Status != nil { - status = *resp.JSON200.Status - } - - switch status { + switch job.Status { case "completed": data := make(map[string]any) - if resp.JSON200.Result != nil { - if m, ok := resp.JSON200.Result.(map[string]any); ok { + if job.Result != nil { + if m, ok := job.Result.(map[string]any); ok { data = m } } @@ -575,8 +541,8 @@ func (r *runner) pollJob( return &Result{Changed: changed, Data: data}, nil case "failed": errMsg := "job failed" - if resp.JSON200.Error != nil { - errMsg = *resp.JSON200.Error + if job.Error != "" { + errMsg = job.Error } return nil, fmt.Errorf("job %s: %s", jobID, errMsg) @@ -585,22 +551,6 @@ func (r *runner) pollJob( } } -// messageFromResponse extracts a human-readable error message from -// generated ErrorResponse pointers. It returns the first non-nil -// error message found, or falls back to the HTTP status code. -func messageFromResponse( - statusCode int, - responses ...*gen.ErrorResponse, -) string { - for _, r := range responses { - if r != nil && r.Error != nil { - return *r.Error - } - } - - return fmt.Sprintf("unexpected status %d", statusCode) -} - // levelize groups tasks into levels where all tasks in a level can // run concurrently (all dependencies are in earlier levels). func levelize( diff --git a/pkg/orchestrator/runner_broadcast_test.go b/pkg/orchestrator/runner_broadcast_test.go index 5be2496..41747ca 100644 --- a/pkg/orchestrator/runner_broadcast_test.go +++ b/pkg/orchestrator/runner_broadcast_test.go @@ -1,9 +1,17 @@ package orchestrator import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" ) type RunnerBroadcastTestSuite struct { @@ -115,6 +123,29 @@ func (s *RunnerBroadcastTestSuite) TestExtractHostResults() { }, want: []HostResult{}, }, + { + name: "non-map item in results array is skipped", + data: map[string]any{ + "results": []any{ + "not-a-map", + 42, + map[string]any{ + "hostname": "host-1", + "changed": true, + }, + }, + }, + want: []HostResult{ + { + Hostname: "host-1", + Changed: true, + Data: map[string]any{ + "hostname": "host-1", + "changed": true, + }, + }, + }, + }, } for _, tt := range tests { @@ -160,3 +191,170 @@ func (s *RunnerBroadcastTestSuite) TestIsCommandOp() { }) } } + +// jobTestServer creates an httptest server that handles POST /job +// and GET /job/{id} with the provided result payload. +func jobTestServer( + jobResult map[string]any, +) *httptest.Server { + const jobID = "11111111-1111-1111-1111-111111111111" + + return httptest.NewServer(http.HandlerFunc(func( + w http.ResponseWriter, + r *http.Request, + ) { + switch { + case r.Method == http.MethodPost && r.URL.Path == "/job": + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + resp := map[string]any{ + "job_id": jobID, + "status": "created", + } + _ = json.NewEncoder(w).Encode(resp) + + case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf("/job/%s", jobID): + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + resp := map[string]any{ + "id": jobID, + "status": "completed", + "result": jobResult, + } + _ = json.NewEncoder(w).Encode(resp) + + default: + w.WriteHeader(http.StatusNotFound) + } + })) +} + +func (s *RunnerBroadcastTestSuite) TestExecuteOpBroadcast() { + tests := []struct { + name string + jobResult map[string]any + wantHostResults int + wantHostname string + }{ + { + name: "broadcast op extracts host results", + jobResult: map[string]any{ + "results": []any{ + map[string]any{ + "hostname": "host-1", + "changed": true, + }, + map[string]any{ + "hostname": "host-2", + "changed": false, + }, + }, + }, + wantHostResults: 2, + wantHostname: "host-1", + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + origInterval := DefaultPollInterval + DefaultPollInterval = 10 * time.Millisecond + + defer func() { + DefaultPollInterval = origInterval + }() + + srv := jobTestServer(tt.jobResult) + defer srv.Close() + + client := osapi.New(srv.URL, "test-token") + plan := NewPlan(client, OnError(StopAll)) + + plan.Task("broadcast-op", &Op{ + Operation: "node.hostname.get", + Target: "_all", + }) + + report, err := plan.Run(context.Background()) + + s.Require().NoError(err) + s.Require().Len(report.Tasks, 1) + s.Len( + report.Tasks[0].HostResults, + tt.wantHostResults, + ) + s.Equal( + tt.wantHostname, + report.Tasks[0].HostResults[0].Hostname, + ) + }) + } +} + +func (s *RunnerBroadcastTestSuite) TestExecuteOpCommandNonZeroExit() { + tests := []struct { + name string + operation string + jobResult map[string]any + wantErr string + }{ + { + name: "command exec with non-zero exit code fails", + operation: "command.exec.execute", + jobResult: map[string]any{ + "exit_code": float64(1), + "stdout": "", + "stderr": "command not found", + }, + wantErr: "command exited with code 1", + }, + { + name: "command shell with non-zero exit code fails", + operation: "command.shell.execute", + jobResult: map[string]any{ + "exit_code": float64(127), + "stdout": "", + "stderr": "not found", + }, + wantErr: "command exited with code 127", + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + origInterval := DefaultPollInterval + DefaultPollInterval = 10 * time.Millisecond + + defer func() { + DefaultPollInterval = origInterval + }() + + srv := jobTestServer(tt.jobResult) + defer srv.Close() + + client := osapi.New(srv.URL, "test-token") + plan := NewPlan(client, OnError(Continue)) + + plan.Task("cmd-op", &Op{ + Operation: tt.operation, + Target: "_any", + Params: map[string]any{"command": "false"}, + }) + + report, err := plan.Run(context.Background()) + + // With Continue strategy, run() doesn't return + // the error, but the task result carries it. + _ = err + s.Require().Len(report.Tasks, 1) + s.Equal(StatusFailed, report.Tasks[0].Status) + s.Require().NotNil(report.Tasks[0].Error) + s.Contains( + report.Tasks[0].Error.Error(), + tt.wantErr, + ) + }) + } +} diff --git a/pkg/osapi/agent.go b/pkg/osapi/agent.go index 5c52331..ff2329d 100644 --- a/pkg/osapi/agent.go +++ b/pkg/osapi/agent.go @@ -22,6 +22,7 @@ package osapi import ( "context" + "fmt" "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" ) @@ -34,14 +35,46 @@ type AgentService struct { // List retrieves all active agents. func (s *AgentService) List( ctx context.Context, -) (*gen.GetAgentResponse, error) { - return s.client.GetAgentWithResponse(ctx) +) (*Response[AgentList], error) { + resp, err := s.client.GetAgentWithResponse(ctx) + if err != nil { + return nil, fmt.Errorf("list agents: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(agentListFromGen(resp.JSON200), resp.Body), nil } // Get retrieves detailed information about a specific agent by hostname. func (s *AgentService) Get( ctx context.Context, hostname string, -) (*gen.GetAgentDetailsResponse, error) { - return s.client.GetAgentDetailsWithResponse(ctx, hostname) +) (*Response[Agent], error) { + resp, err := s.client.GetAgentDetailsWithResponse(ctx, hostname) + if err != nil { + return nil, fmt.Errorf("get agent %s: %w", hostname, err) + } + + if err := checkError(resp.StatusCode(), resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(agentFromGen(resp.JSON200), resp.Body), nil } diff --git a/pkg/osapi/agent_public_test.go b/pkg/osapi/agent_public_test.go index 139a7bf..0ecbef6 100644 --- a/pkg/osapi/agent_public_test.go +++ b/pkg/osapi/agent_public_test.go @@ -22,6 +22,7 @@ package osapi_test import ( "context" + "errors" "log/slog" "net/http" "net/http/httptest" @@ -35,50 +36,127 @@ import ( type AgentPublicTestSuite struct { suite.Suite - ctx context.Context - server *httptest.Server - sut *osapi.Client + ctx context.Context } func (suite *AgentPublicTestSuite) SetupTest() { suite.ctx = context.Background() - - suite.server = httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{}`)) - }), - ) - - suite.sut = osapi.New( - suite.server.URL, - "test-token", - osapi.WithLogger(slog.Default()), - ) -} - -func (suite *AgentPublicTestSuite) TearDownTest() { - suite.server.Close() } func (suite *AgentPublicTestSuite) TestList() { tests := []struct { name string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.AgentList], error) }{ { name: "when requesting agents returns no error", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.AgentList], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal(0, resp.Data.Total) + suite.Empty(resp.Data.Agents) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Agent.List(suite.ctx) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"agents":[],"total":0}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Agent.List(suite.ctx) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *AgentPublicTestSuite) TestListError() { + tests := []struct { + name string + serverURL string + serverFunc func() *httptest.Server + validateFunc func(*osapi.Response[osapi.AgentList], error) + }{ + { + name: "when server returns 401 returns AuthError", + serverFunc: func() *httptest.Server { + return httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + ) + }, + validateFunc: func(resp *osapi.Response[osapi.AgentList], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusUnauthorized, target.StatusCode) + }, + }, + { + name: "when client HTTP error returns wrapped error", + serverURL: "http://127.0.0.1:0", + validateFunc: func(resp *osapi.Response[osapi.AgentList], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "list agents") + }, + }, + { + name: "when response JSON200 is nil returns UnexpectedStatusError", + serverFunc: func() *httptest.Server { + return httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + }), + ) + }, + validateFunc: func(resp *osapi.Response[osapi.AgentList], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Contains(target.Message, "nil response body") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + url := tc.serverURL + if tc.serverFunc != nil { + server := tc.serverFunc() + defer server.Close() + url = server.URL + } + + sut := osapi.New( + url, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Agent.List(suite.ctx) + tc.validateFunc(resp, err) }) } } @@ -87,21 +165,119 @@ func (suite *AgentPublicTestSuite) TestGet() { tests := []struct { name string hostname string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Agent], error) }{ { name: "when requesting agent details returns no error", hostname: "server1", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.Agent], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal("server1", resp.Data.Hostname) + suite.Equal("Ready", resp.Data.Status) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Agent.Get(suite.ctx, tc.hostname) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"hostname":"server1","status":"Ready"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Agent.Get(suite.ctx, tc.hostname) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *AgentPublicTestSuite) TestGetError() { + tests := []struct { + name string + serverURL string + serverFunc func() *httptest.Server + validateFunc func(*osapi.Response[osapi.Agent], error) + }{ + { + name: "when server returns 404 returns NotFoundError", + serverFunc: func() *httptest.Server { + return httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"error":"agent not found"}`)) + }), + ) + }, + validateFunc: func(resp *osapi.Response[osapi.Agent], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.NotFoundError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusNotFound, target.StatusCode) + suite.Equal("agent not found", target.Message) + }, + }, + { + name: "when client HTTP error returns wrapped error", + serverURL: "http://127.0.0.1:0", + validateFunc: func(resp *osapi.Response[osapi.Agent], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get agent") + }, + }, + { + name: "when response JSON200 is nil returns UnexpectedStatusError", + serverFunc: func() *httptest.Server { + return httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + }), + ) + }, + validateFunc: func(resp *osapi.Response[osapi.Agent], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Contains(target.Message, "nil response body") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + url := tc.serverURL + if tc.serverFunc != nil { + server := tc.serverFunc() + defer server.Close() + url = server.URL + } + + sut := osapi.New( + url, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Agent.Get(suite.ctx, "unknown-host") + tc.validateFunc(resp, err) }) } } diff --git a/pkg/osapi/agent_types.go b/pkg/osapi/agent_types.go new file mode 100644 index 0000000..bfb14d7 --- /dev/null +++ b/pkg/osapi/agent_types.go @@ -0,0 +1,187 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + "time" + + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +// Agent represents a registered OSAPI agent. +type Agent struct { + Hostname string + Status string + Labels map[string]string + Architecture string + CPUCount int + Fqdn string + KernelVersion string + PackageMgr string + ServiceMgr string + LoadAverage *LoadAverage + Memory *Memory + OSInfo *OSInfo + Interfaces []NetworkInterface + Uptime string + StartedAt time.Time + RegisteredAt time.Time + Facts map[string]any +} + +// AgentList is a collection of agents. +type AgentList struct { + Agents []Agent + Total int +} + +// NetworkInterface represents a network interface on an agent. +type NetworkInterface struct { + Name string + Family string + IPv4 string + IPv6 string + MAC string +} + +// LoadAverage represents system load averages. +type LoadAverage struct { + OneMin float32 + FiveMin float32 + FifteenMin float32 +} + +// Memory represents memory usage information. +type Memory struct { + Total int + Used int + Free int +} + +// OSInfo represents operating system information. +type OSInfo struct { + Distribution string + Version string +} + +// agentFromGen converts a gen.AgentInfo to an Agent. +func agentFromGen( + g *gen.AgentInfo, +) Agent { + a := Agent{ + Hostname: g.Hostname, + Status: string(g.Status), + } + + if g.Labels != nil { + a.Labels = *g.Labels + } + + if g.Architecture != nil { + a.Architecture = *g.Architecture + } + + if g.CpuCount != nil { + a.CPUCount = *g.CpuCount + } + + if g.Fqdn != nil { + a.Fqdn = *g.Fqdn + } + + if g.KernelVersion != nil { + a.KernelVersion = *g.KernelVersion + } + + if g.PackageMgr != nil { + a.PackageMgr = *g.PackageMgr + } + + if g.ServiceMgr != nil { + a.ServiceMgr = *g.ServiceMgr + } + + a.LoadAverage = loadAverageFromGen(g.LoadAverage) + a.Memory = memoryFromGen(g.Memory) + a.OSInfo = osInfoFromGen(g.OsInfo) + + if g.Interfaces != nil { + ifaces := make([]NetworkInterface, 0, len(*g.Interfaces)) + for _, iface := range *g.Interfaces { + ni := NetworkInterface{ + Name: iface.Name, + } + + if iface.Family != nil { + ni.Family = string(*iface.Family) + } + + if iface.Ipv4 != nil { + ni.IPv4 = *iface.Ipv4 + } + + if iface.Ipv6 != nil { + ni.IPv6 = *iface.Ipv6 + } + + if iface.Mac != nil { + ni.MAC = *iface.Mac + } + + ifaces = append(ifaces, ni) + } + + a.Interfaces = ifaces + } + + if g.Uptime != nil { + a.Uptime = *g.Uptime + } + + if g.StartedAt != nil { + a.StartedAt = *g.StartedAt + } + + if g.RegisteredAt != nil { + a.RegisteredAt = *g.RegisteredAt + } + + if g.Facts != nil { + a.Facts = *g.Facts + } + + return a +} + +// agentListFromGen converts a gen.ListAgentsResponse to an AgentList. +func agentListFromGen( + g *gen.ListAgentsResponse, +) AgentList { + agents := make([]Agent, 0, len(g.Agents)) + for i := range g.Agents { + agents = append(agents, agentFromGen(&g.Agents[i])) + } + + return AgentList{ + Agents: agents, + Total: g.Total, + } +} diff --git a/pkg/osapi/agent_types_test.go b/pkg/osapi/agent_types_test.go new file mode 100644 index 0000000..7414b31 --- /dev/null +++ b/pkg/osapi/agent_types_test.go @@ -0,0 +1,243 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +type AgentTypesTestSuite struct { + suite.Suite +} + +func (suite *AgentTypesTestSuite) TestAgentFromGen() { + now := time.Now().UTC().Truncate(time.Second) + startedAt := now.Add(-1 * time.Hour) + + tests := []struct { + name string + input *gen.AgentInfo + validateFunc func(Agent) + }{ + { + name: "when all fields are populated", + input: func() *gen.AgentInfo { + labels := map[string]string{"group": "web", "env": "prod"} + arch := "amd64" + cpuCount := 8 + fqdn := "web-01.example.com" + kernelVersion := "5.15.0-generic" + packageMgr := "apt" + serviceMgr := "systemd" + uptime := "5d 3h 22m" + family := gen.NetworkInterfaceResponseFamily("inet") + ipv4 := "192.168.1.10" + ipv6 := "fe80::1" + mac := "00:11:22:33:44:55" + facts := map[string]interface{}{"custom_key": "custom_value"} + + return &gen.AgentInfo{ + Hostname: "web-01", + Status: gen.AgentInfoStatus("Ready"), + Labels: &labels, + Architecture: &arch, + CpuCount: &cpuCount, + Fqdn: &fqdn, + KernelVersion: &kernelVersion, + PackageMgr: &packageMgr, + ServiceMgr: &serviceMgr, + LoadAverage: &gen.LoadAverageResponse{ + N1min: 0.5, + N5min: 1.2, + N15min: 0.8, + }, + Memory: &gen.MemoryResponse{ + Total: 8589934592, + Used: 4294967296, + Free: 4294967296, + }, + OsInfo: &gen.OSInfoResponse{ + Distribution: "Ubuntu", + Version: "22.04", + }, + Interfaces: &[]gen.NetworkInterfaceResponse{ + { + Name: "eth0", + Family: &family, + Ipv4: &ipv4, + Ipv6: &ipv6, + Mac: &mac, + }, + }, + Uptime: &uptime, + StartedAt: &startedAt, + RegisteredAt: &now, + Facts: &facts, + } + }(), + validateFunc: func(a Agent) { + suite.Equal("web-01", a.Hostname) + suite.Equal("Ready", a.Status) + suite.Equal(map[string]string{"group": "web", "env": "prod"}, a.Labels) + suite.Equal("amd64", a.Architecture) + suite.Equal(8, a.CPUCount) + suite.Equal("web-01.example.com", a.Fqdn) + suite.Equal("5.15.0-generic", a.KernelVersion) + suite.Equal("apt", a.PackageMgr) + suite.Equal("systemd", a.ServiceMgr) + + suite.Require().NotNil(a.LoadAverage) + suite.InDelta(0.5, float64(a.LoadAverage.OneMin), 0.001) + suite.InDelta(1.2, float64(a.LoadAverage.FiveMin), 0.001) + suite.InDelta(0.8, float64(a.LoadAverage.FifteenMin), 0.001) + + suite.Require().NotNil(a.Memory) + suite.Equal(8589934592, a.Memory.Total) + suite.Equal(4294967296, a.Memory.Used) + suite.Equal(4294967296, a.Memory.Free) + + suite.Require().NotNil(a.OSInfo) + suite.Equal("Ubuntu", a.OSInfo.Distribution) + suite.Equal("22.04", a.OSInfo.Version) + + suite.Require().Len(a.Interfaces, 1) + suite.Equal("eth0", a.Interfaces[0].Name) + suite.Equal("inet", a.Interfaces[0].Family) + suite.Equal("192.168.1.10", a.Interfaces[0].IPv4) + suite.Equal("fe80::1", a.Interfaces[0].IPv6) + suite.Equal("00:11:22:33:44:55", a.Interfaces[0].MAC) + + suite.Equal("5d 3h 22m", a.Uptime) + suite.Equal(startedAt, a.StartedAt) + suite.Equal(now, a.RegisteredAt) + suite.Equal(map[string]any{"custom_key": "custom_value"}, a.Facts) + }, + }, + { + name: "when only required fields are set", + input: &gen.AgentInfo{ + Hostname: "minimal-host", + Status: gen.AgentInfoStatus("Ready"), + }, + validateFunc: func(a Agent) { + suite.Equal("minimal-host", a.Hostname) + suite.Equal("Ready", a.Status) + suite.Nil(a.Labels) + suite.Empty(a.Architecture) + suite.Zero(a.CPUCount) + suite.Empty(a.Fqdn) + suite.Empty(a.KernelVersion) + suite.Empty(a.PackageMgr) + suite.Empty(a.ServiceMgr) + suite.Nil(a.LoadAverage) + suite.Nil(a.Memory) + suite.Nil(a.OSInfo) + suite.Nil(a.Interfaces) + suite.Empty(a.Uptime) + suite.True(a.StartedAt.IsZero()) + suite.True(a.RegisteredAt.IsZero()) + suite.Nil(a.Facts) + }, + }, + { + name: "when interfaces list is empty", + input: func() *gen.AgentInfo { + ifaces := []gen.NetworkInterfaceResponse{} + + return &gen.AgentInfo{ + Hostname: "no-ifaces", + Status: gen.AgentInfoStatus("Ready"), + Interfaces: &ifaces, + } + }(), + validateFunc: func(a Agent) { + suite.Equal("no-ifaces", a.Hostname) + suite.NotNil(a.Interfaces) + suite.Empty(a.Interfaces) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := agentFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *AgentTypesTestSuite) TestAgentListFromGen() { + tests := []struct { + name string + input *gen.ListAgentsResponse + validateFunc func(AgentList) + }{ + { + name: "when list contains agents", + input: &gen.ListAgentsResponse{ + Agents: []gen.AgentInfo{ + { + Hostname: "web-01", + Status: gen.AgentInfoStatus("Ready"), + }, + { + Hostname: "web-02", + Status: gen.AgentInfoStatus("Ready"), + }, + }, + Total: 2, + }, + validateFunc: func(al AgentList) { + suite.Equal(2, al.Total) + suite.Require().Len(al.Agents, 2) + suite.Equal("web-01", al.Agents[0].Hostname) + suite.Equal("web-02", al.Agents[1].Hostname) + }, + }, + { + name: "when list is empty", + input: &gen.ListAgentsResponse{ + Agents: []gen.AgentInfo{}, + Total: 0, + }, + validateFunc: func(al AgentList) { + suite.Equal(0, al.Total) + suite.Empty(al.Agents) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := agentListFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func TestAgentTypesTestSuite(t *testing.T) { + suite.Run(t, new(AgentTypesTestSuite)) +} diff --git a/pkg/osapi/audit.go b/pkg/osapi/audit.go index 4de5637..5a04462 100644 --- a/pkg/osapi/audit.go +++ b/pkg/osapi/audit.go @@ -22,6 +22,7 @@ package osapi import ( "context" + "fmt" "github.com/google/uuid" @@ -38,31 +39,96 @@ func (s *AuditService) List( ctx context.Context, limit int, offset int, -) (*gen.GetAuditLogsResponse, error) { +) (*Response[AuditList], error) { params := &gen.GetAuditLogsParams{ Limit: &limit, Offset: &offset, } - return s.client.GetAuditLogsWithResponse(ctx, params) + resp, err := s.client.GetAuditLogsWithResponse(ctx, params) + if err != nil { + return nil, fmt.Errorf("list audit logs: %w", err) + } + + if err := checkError( + resp.StatusCode(), + resp.JSON400, + resp.JSON401, + resp.JSON403, + resp.JSON500, + ); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(auditListFromGen(resp.JSON200), resp.Body), nil } // Get retrieves a single audit log entry by ID. func (s *AuditService) Get( ctx context.Context, id string, -) (*gen.GetAuditLogByIDResponse, error) { +) (*Response[AuditEntry], error) { parsedID, err := uuid.Parse(id) if err != nil { return nil, err } - return s.client.GetAuditLogByIDWithResponse(ctx, parsedID) + resp, err := s.client.GetAuditLogByIDWithResponse(ctx, parsedID) + if err != nil { + return nil, fmt.Errorf("get audit log %s: %w", id, err) + } + + if err := checkError( + resp.StatusCode(), + resp.JSON401, + resp.JSON403, + resp.JSON404, + resp.JSON500, + ); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(auditEntryFromGen(resp.JSON200.Entry), resp.Body), nil } // Export retrieves all audit log entries for export. func (s *AuditService) Export( ctx context.Context, -) (*gen.GetAuditExportResponse, error) { - return s.client.GetAuditExportWithResponse(ctx) +) (*Response[AuditList], error) { + resp, err := s.client.GetAuditExportWithResponse(ctx) + if err != nil { + return nil, fmt.Errorf("export audit logs: %w", err) + } + + if err := checkError( + resp.StatusCode(), + resp.JSON401, + resp.JSON403, + resp.JSON500, + ); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(auditListFromGen(resp.JSON200), resp.Body), nil } diff --git a/pkg/osapi/audit_public_test.go b/pkg/osapi/audit_public_test.go index bb41777..96adadd 100644 --- a/pkg/osapi/audit_public_test.go +++ b/pkg/osapi/audit_public_test.go @@ -22,6 +22,7 @@ package osapi_test import ( "context" + "errors" "log/slog" "net/http" "net/http/httptest" @@ -35,31 +36,11 @@ import ( type AuditPublicTestSuite struct { suite.Suite - ctx context.Context - server *httptest.Server - sut *osapi.Client + ctx context.Context } func (suite *AuditPublicTestSuite) SetupTest() { suite.ctx = context.Background() - - suite.server = httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{}`)) - }), - ) - - suite.sut = osapi.New( - suite.server.URL, - "test-token", - osapi.WithLogger(slog.Default()), - ) -} - -func (suite *AuditPublicTestSuite) TearDownTest() { - suite.server.Close() } func (suite *AuditPublicTestSuite) TestList() { @@ -67,22 +48,121 @@ func (suite *AuditPublicTestSuite) TestList() { name string limit int offset int - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.AuditList], error) }{ { - name: "when listing audit entries returns no error", + name: "when listing audit entries returns audit list", limit: 20, offset: 0, - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.AuditList], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal(0, resp.Data.TotalItems) + suite.Empty(resp.Data.Items) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Audit.List(suite.ctx, tc.limit, tc.offset) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"items":[],"total_items":0}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Audit.List(suite.ctx, tc.limit, tc.offset) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *AuditPublicTestSuite) TestListError() { + tests := []struct { + name string + setupServer func() *httptest.Server + validateFunc func(*osapi.Response[osapi.AuditList], error) + }{ + { + name: "when server returns 401 returns AuthError", + setupServer: func() *httptest.Server { + return httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + ) + }, + validateFunc: func(resp *osapi.Response[osapi.AuditList], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusUnauthorized, target.StatusCode) + }, + }, + { + name: "when HTTP request fails returns error", + setupServer: func() *httptest.Server { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + return server + }, + validateFunc: func(resp *osapi.Response[osapi.AuditList], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "list audit logs:") + }, + }, + { + name: "when response body is nil returns UnexpectedStatusError", + setupServer: func() *httptest.Server { + return httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + }), + ) + }, + validateFunc: func(resp *osapi.Response[osapi.AuditList], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Contains(target.Message, "nil response body") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := tc.setupServer() + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Audit.List(suite.ctx, 20, 0) + tc.validateFunc(resp, err) }) } } @@ -91,28 +171,134 @@ func (suite *AuditPublicTestSuite) TestGet() { tests := []struct { name string id string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.AuditEntry], error) }{ { - name: "when valid UUID returns no error", + name: "when valid UUID returns audit entry", id: "550e8400-e29b-41d4-a716-446655440000", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.AuditEntry], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal("550e8400-e29b-41d4-a716-446655440000", resp.Data.ID) + suite.Equal("admin", resp.Data.User) + suite.Equal("GET", resp.Data.Method) + suite.Equal("/api/v1/health", resp.Data.Path) }, }, { name: "when invalid UUID returns error", id: "not-a-uuid", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.AuditEntry], err error) { + suite.Error(err) + suite.Nil(resp) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"entry":{"id":"550e8400-e29b-41d4-a716-446655440000","timestamp":"2026-01-01T00:00:00Z","user":"admin","roles":["admin"],"method":"GET","path":"/api/v1/health","response_code":200,"duration_ms":5,"source_ip":"127.0.0.1"}}`, + ), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Audit.Get(suite.ctx, tc.id) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *AuditPublicTestSuite) TestGetError() { + tests := []struct { + name string + setupServer func() *httptest.Server + validateFunc func(*osapi.Response[osapi.AuditEntry], error) + }{ + { + name: "when server returns 404 returns NotFoundError", + setupServer: func() *httptest.Server { + return httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"error":"audit entry not found"}`)) + }), + ) + }, + validateFunc: func(resp *osapi.Response[osapi.AuditEntry], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.NotFoundError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusNotFound, target.StatusCode) + }, + }, + { + name: "when HTTP request fails returns error", + setupServer: func() *httptest.Server { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + return server + }, + validateFunc: func(resp *osapi.Response[osapi.AuditEntry], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get audit log") + }, + }, + { + name: "when response body is nil returns UnexpectedStatusError", + setupServer: func() *httptest.Server { + return httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + }), + ) + }, + validateFunc: func(resp *osapi.Response[osapi.AuditEntry], err error) { suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Contains(target.Message, "nil response body") }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Audit.Get(suite.ctx, tc.id) - tc.validateFunc(err) + server := tc.setupServer() + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Audit.Get(suite.ctx, "550e8400-e29b-41d4-a716-446655440000") + tc.validateFunc(resp, err) }) } } @@ -120,20 +306,119 @@ func (suite *AuditPublicTestSuite) TestGet() { func (suite *AuditPublicTestSuite) TestExport() { tests := []struct { name string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.AuditList], error) }{ { - name: "when exporting audit entries returns no error", - validateFunc: func(err error) { + name: "when exporting audit entries returns audit list", + validateFunc: func(resp *osapi.Response[osapi.AuditList], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal(0, resp.Data.TotalItems) + suite.Empty(resp.Data.Items) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Audit.Export(suite.ctx) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"items":[],"total_items":0}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Audit.Export(suite.ctx) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *AuditPublicTestSuite) TestExportError() { + tests := []struct { + name string + setupServer func() *httptest.Server + validateFunc func(*osapi.Response[osapi.AuditList], error) + }{ + { + name: "when server returns 401 returns AuthError", + setupServer: func() *httptest.Server { + return httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + ) + }, + validateFunc: func(resp *osapi.Response[osapi.AuditList], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusUnauthorized, target.StatusCode) + }, + }, + { + name: "when HTTP request fails returns error", + setupServer: func() *httptest.Server { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + return server + }, + validateFunc: func(resp *osapi.Response[osapi.AuditList], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "export audit logs:") + }, + }, + { + name: "when response body is nil returns UnexpectedStatusError", + setupServer: func() *httptest.Server { + return httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + }), + ) + }, + validateFunc: func(resp *osapi.Response[osapi.AuditList], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Contains(target.Message, "nil response body") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := tc.setupServer() + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Audit.Export(suite.ctx) + tc.validateFunc(resp, err) }) } } diff --git a/pkg/osapi/audit_types.go b/pkg/osapi/audit_types.go new file mode 100644 index 0000000..00d43f5 --- /dev/null +++ b/pkg/osapi/audit_types.go @@ -0,0 +1,85 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + "time" + + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +// AuditEntry represents a single audit log entry. +type AuditEntry struct { + ID string + Timestamp time.Time + User string + Roles []string + Method string + Path string + ResponseCode int + DurationMs int64 + SourceIP string + OperationID string +} + +// AuditList is a paginated list of audit entries. +type AuditList struct { + Items []AuditEntry + TotalItems int +} + +// auditEntryFromGen converts a gen.AuditEntry to an AuditEntry. +func auditEntryFromGen( + g gen.AuditEntry, +) AuditEntry { + a := AuditEntry{ + ID: g.Id.String(), + Timestamp: g.Timestamp, + User: g.User, + Roles: g.Roles, + Method: g.Method, + Path: g.Path, + ResponseCode: g.ResponseCode, + DurationMs: g.DurationMs, + SourceIP: g.SourceIp, + } + + if g.OperationId != nil { + a.OperationID = *g.OperationId + } + + return a +} + +// auditListFromGen converts a gen.ListAuditResponse to an AuditList. +func auditListFromGen( + g *gen.ListAuditResponse, +) AuditList { + items := make([]AuditEntry, 0, len(g.Items)) + for _, entry := range g.Items { + items = append(items, auditEntryFromGen(entry)) + } + + return AuditList{ + Items: items, + TotalItems: g.TotalItems, + } +} diff --git a/pkg/osapi/audit_types_test.go b/pkg/osapi/audit_types_test.go new file mode 100644 index 0000000..218554d --- /dev/null +++ b/pkg/osapi/audit_types_test.go @@ -0,0 +1,233 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + "testing" + "time" + + openapi_types "github.com/oapi-codegen/runtime/types" + "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +type AuditTypesTestSuite struct { + suite.Suite +} + +func (suite *AuditTypesTestSuite) TestAuditEntryFromGen() { + now := time.Now().UTC().Truncate(time.Second) + testUUID := openapi_types.UUID{ + 0x55, + 0x0e, + 0x84, + 0x00, + 0xe2, + 0x9b, + 0x41, + 0xd4, + 0xa7, + 0x16, + 0x44, + 0x66, + 0x55, + 0x44, + 0x00, + 0x00, + } + operationID := "getNodeHostname" + + tests := []struct { + name string + input gen.AuditEntry + validateFunc func(AuditEntry) + }{ + { + name: "when all fields are populated", + input: gen.AuditEntry{ + Id: testUUID, + Timestamp: now, + User: "admin@example.com", + Roles: []string{"admin", "write"}, + Method: "GET", + Path: "/api/v1/node/web-01", + ResponseCode: 200, + DurationMs: 42, + SourceIp: "192.168.1.100", + OperationId: &operationID, + }, + validateFunc: func(a AuditEntry) { + suite.Equal("550e8400-e29b-41d4-a716-446655440000", a.ID) + suite.Equal(now, a.Timestamp) + suite.Equal("admin@example.com", a.User) + suite.Equal([]string{"admin", "write"}, a.Roles) + suite.Equal("GET", a.Method) + suite.Equal("/api/v1/node/web-01", a.Path) + suite.Equal(200, a.ResponseCode) + suite.Equal(int64(42), a.DurationMs) + suite.Equal("192.168.1.100", a.SourceIP) + suite.Equal("getNodeHostname", a.OperationID) + }, + }, + { + name: "when OperationId is nil", + input: gen.AuditEntry{ + Id: testUUID, + Timestamp: now, + User: "user@example.com", + Roles: []string{"read"}, + Method: "POST", + Path: "/api/v1/jobs", + ResponseCode: 201, + DurationMs: 15, + SourceIp: "10.0.0.1", + OperationId: nil, + }, + validateFunc: func(a AuditEntry) { + suite.Equal("550e8400-e29b-41d4-a716-446655440000", a.ID) + suite.Equal(now, a.Timestamp) + suite.Equal("user@example.com", a.User) + suite.Equal([]string{"read"}, a.Roles) + suite.Equal("POST", a.Method) + suite.Equal("/api/v1/jobs", a.Path) + suite.Equal(201, a.ResponseCode) + suite.Equal(int64(15), a.DurationMs) + suite.Equal("10.0.0.1", a.SourceIP) + suite.Empty(a.OperationID) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := auditEntryFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *AuditTypesTestSuite) TestAuditListFromGen() { + now := time.Now().UTC().Truncate(time.Second) + testUUID1 := openapi_types.UUID{ + 0x55, + 0x0e, + 0x84, + 0x00, + 0xe2, + 0x9b, + 0x41, + 0xd4, + 0xa7, + 0x16, + 0x44, + 0x66, + 0x55, + 0x44, + 0x00, + 0x01, + } + testUUID2 := openapi_types.UUID{ + 0x55, + 0x0e, + 0x84, + 0x00, + 0xe2, + 0x9b, + 0x41, + 0xd4, + 0xa7, + 0x16, + 0x44, + 0x66, + 0x55, + 0x44, + 0x00, + 0x02, + } + + tests := []struct { + name string + input *gen.ListAuditResponse + validateFunc func(AuditList) + }{ + { + name: "when list contains items", + input: &gen.ListAuditResponse{ + Items: []gen.AuditEntry{ + { + Id: testUUID1, + Timestamp: now, + User: "admin@example.com", + Roles: []string{"admin"}, + Method: "GET", + Path: "/api/v1/health", + ResponseCode: 200, + DurationMs: 5, + SourceIp: "192.168.1.1", + }, + { + Id: testUUID2, + Timestamp: now, + User: "user@example.com", + Roles: []string{"read"}, + Method: "POST", + Path: "/api/v1/jobs", + ResponseCode: 201, + DurationMs: 30, + SourceIp: "10.0.0.1", + }, + }, + TotalItems: 2, + }, + validateFunc: func(al AuditList) { + suite.Equal(2, al.TotalItems) + suite.Require().Len(al.Items, 2) + suite.Equal("550e8400-e29b-41d4-a716-446655440001", al.Items[0].ID) + suite.Equal("admin@example.com", al.Items[0].User) + suite.Equal("550e8400-e29b-41d4-a716-446655440002", al.Items[1].ID) + suite.Equal("user@example.com", al.Items[1].User) + }, + }, + { + name: "when list is empty", + input: &gen.ListAuditResponse{ + Items: []gen.AuditEntry{}, + TotalItems: 0, + }, + validateFunc: func(al AuditList) { + suite.Equal(0, al.TotalItems) + suite.Empty(al.Items) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := auditListFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func TestAuditTypesTestSuite(t *testing.T) { + suite.Run(t, new(AuditTypesTestSuite)) +} diff --git a/pkg/osapi/errors.go b/pkg/osapi/errors.go new file mode 100644 index 0000000..51854bc --- /dev/null +++ b/pkg/osapi/errors.go @@ -0,0 +1,88 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import "fmt" + +// APIError is the base error type for OSAPI API errors. +type APIError struct { + StatusCode int + Message string +} + +// Error returns a formatted error string. +func (e *APIError) Error() string { + return fmt.Sprintf( + "api error (status %d): %s", + e.StatusCode, + e.Message, + ) +} + +// AuthError represents authentication/authorization errors (401, 403). +type AuthError struct { + APIError +} + +// Unwrap returns the underlying APIError. +func (e *AuthError) Unwrap() error { + return &e.APIError +} + +// NotFoundError represents resource not found errors (404). +type NotFoundError struct { + APIError +} + +// Unwrap returns the underlying APIError. +func (e *NotFoundError) Unwrap() error { + return &e.APIError +} + +// ValidationError represents validation errors (400). +type ValidationError struct { + APIError +} + +// Unwrap returns the underlying APIError. +func (e *ValidationError) Unwrap() error { + return &e.APIError +} + +// ServerError represents internal server errors (500). +type ServerError struct { + APIError +} + +// Unwrap returns the underlying APIError. +func (e *ServerError) Unwrap() error { + return &e.APIError +} + +// UnexpectedStatusError represents unexpected HTTP status codes. +type UnexpectedStatusError struct { + APIError +} + +// Unwrap returns the underlying APIError. +func (e *UnexpectedStatusError) Unwrap() error { + return &e.APIError +} diff --git a/pkg/osapi/errors_public_test.go b/pkg/osapi/errors_public_test.go new file mode 100644 index 0000000..c4e1c34 --- /dev/null +++ b/pkg/osapi/errors_public_test.go @@ -0,0 +1,322 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +type ErrorsPublicTestSuite struct { + suite.Suite +} + +func (suite *ErrorsPublicTestSuite) TestErrorFormat() { + tests := []struct { + name string + err error + validateFunc func(error) + }{ + { + name: "when APIError formats correctly", + err: &osapi.APIError{ + StatusCode: 500, + Message: "something went wrong", + }, + validateFunc: func(err error) { + suite.Equal( + "api error (status 500): something went wrong", + err.Error(), + ) + }, + }, + { + name: "when AuthError formats correctly", + err: &osapi.AuthError{ + APIError: osapi.APIError{ + StatusCode: 401, + Message: "unauthorized", + }, + }, + validateFunc: func(err error) { + suite.Equal( + "api error (status 401): unauthorized", + err.Error(), + ) + }, + }, + { + name: "when NotFoundError formats correctly", + err: &osapi.NotFoundError{ + APIError: osapi.APIError{ + StatusCode: 404, + Message: "resource not found", + }, + }, + validateFunc: func(err error) { + suite.Equal( + "api error (status 404): resource not found", + err.Error(), + ) + }, + }, + { + name: "when ValidationError formats correctly", + err: &osapi.ValidationError{ + APIError: osapi.APIError{ + StatusCode: 400, + Message: "invalid input", + }, + }, + validateFunc: func(err error) { + suite.Equal( + "api error (status 400): invalid input", + err.Error(), + ) + }, + }, + { + name: "when ServerError formats correctly", + err: &osapi.ServerError{ + APIError: osapi.APIError{ + StatusCode: 500, + Message: "internal server error", + }, + }, + validateFunc: func(err error) { + suite.Equal( + "api error (status 500): internal server error", + err.Error(), + ) + }, + }, + { + name: "when UnexpectedStatusError formats correctly", + err: &osapi.UnexpectedStatusError{ + APIError: osapi.APIError{ + StatusCode: 418, + Message: "unexpected status", + }, + }, + validateFunc: func(err error) { + suite.Equal( + "api error (status 418): unexpected status", + err.Error(), + ) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + tc.validateFunc(tc.err) + }) + } +} + +func (suite *ErrorsPublicTestSuite) TestErrorsAsUnwrap() { + tests := []struct { + name string + err error + validateFunc func(error) + }{ + { + name: "when AuthError is unwrapped via errors.As", + err: fmt.Errorf("wrapped: %w", &osapi.AuthError{ + APIError: osapi.APIError{ + StatusCode: 403, + Message: "forbidden", + }, + }), + validateFunc: func(err error) { + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(403, target.StatusCode) + suite.Equal("forbidden", target.Message) + }, + }, + { + name: "when NotFoundError is unwrapped via errors.As", + err: fmt.Errorf("wrapped: %w", &osapi.NotFoundError{ + APIError: osapi.APIError{ + StatusCode: 404, + Message: "not found", + }, + }), + validateFunc: func(err error) { + var target *osapi.NotFoundError + suite.True(errors.As(err, &target)) + suite.Equal(404, target.StatusCode) + suite.Equal("not found", target.Message) + }, + }, + { + name: "when ValidationError is unwrapped via errors.As", + err: fmt.Errorf("wrapped: %w", &osapi.ValidationError{ + APIError: osapi.APIError{ + StatusCode: 400, + Message: "bad request", + }, + }), + validateFunc: func(err error) { + var target *osapi.ValidationError + suite.True(errors.As(err, &target)) + suite.Equal(400, target.StatusCode) + suite.Equal("bad request", target.Message) + }, + }, + { + name: "when ServerError is unwrapped via errors.As", + err: fmt.Errorf("wrapped: %w", &osapi.ServerError{ + APIError: osapi.APIError{ + StatusCode: 500, + Message: "server failure", + }, + }), + validateFunc: func(err error) { + var target *osapi.ServerError + suite.True(errors.As(err, &target)) + suite.Equal(500, target.StatusCode) + suite.Equal("server failure", target.Message) + }, + }, + { + name: "when UnexpectedStatusError is unwrapped via errors.As", + err: fmt.Errorf("wrapped: %w", &osapi.UnexpectedStatusError{ + APIError: osapi.APIError{ + StatusCode: 502, + Message: "bad gateway", + }, + }), + validateFunc: func(err error) { + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(502, target.StatusCode) + suite.Equal("bad gateway", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + tc.validateFunc(tc.err) + }) + } +} + +func (suite *ErrorsPublicTestSuite) TestErrorsAsAPIError() { + tests := []struct { + name string + err error + validateFunc func(error) + }{ + { + name: "when AuthError is matchable as APIError", + err: fmt.Errorf("wrapped: %w", &osapi.AuthError{ + APIError: osapi.APIError{ + StatusCode: 401, + Message: "unauthorized", + }, + }), + validateFunc: func(err error) { + var target *osapi.APIError + suite.True(errors.As(err, &target)) + suite.Equal(401, target.StatusCode) + suite.Equal("unauthorized", target.Message) + }, + }, + { + name: "when NotFoundError is matchable as APIError", + err: fmt.Errorf("wrapped: %w", &osapi.NotFoundError{ + APIError: osapi.APIError{ + StatusCode: 404, + Message: "not found", + }, + }), + validateFunc: func(err error) { + var target *osapi.APIError + suite.True(errors.As(err, &target)) + suite.Equal(404, target.StatusCode) + suite.Equal("not found", target.Message) + }, + }, + { + name: "when ValidationError is matchable as APIError", + err: fmt.Errorf("wrapped: %w", &osapi.ValidationError{ + APIError: osapi.APIError{ + StatusCode: 400, + Message: "invalid", + }, + }), + validateFunc: func(err error) { + var target *osapi.APIError + suite.True(errors.As(err, &target)) + suite.Equal(400, target.StatusCode) + suite.Equal("invalid", target.Message) + }, + }, + { + name: "when ServerError is matchable as APIError", + err: fmt.Errorf("wrapped: %w", &osapi.ServerError{ + APIError: osapi.APIError{ + StatusCode: 500, + Message: "internal error", + }, + }), + validateFunc: func(err error) { + var target *osapi.APIError + suite.True(errors.As(err, &target)) + suite.Equal(500, target.StatusCode) + suite.Equal("internal error", target.Message) + }, + }, + { + name: "when UnexpectedStatusError is matchable as APIError", + err: fmt.Errorf("wrapped: %w", &osapi.UnexpectedStatusError{ + APIError: osapi.APIError{ + StatusCode: 418, + Message: "teapot", + }, + }), + validateFunc: func(err error) { + var target *osapi.APIError + suite.True(errors.As(err, &target)) + suite.Equal(418, target.StatusCode) + suite.Equal("teapot", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + tc.validateFunc(tc.err) + }) + } +} + +func TestErrorsPublicTestSuite(t *testing.T) { + suite.Run(t, new(ErrorsPublicTestSuite)) +} diff --git a/pkg/osapi/gen/api.yaml b/pkg/osapi/gen/api.yaml index f9ff981..0dbe61d 100644 --- a/pkg/osapi/gen/api.yaml +++ b/pkg/osapi/gen/api.yaml @@ -1348,6 +1348,38 @@ components: $ref: '#/components/schemas/LoadAverageResponse' memory: $ref: '#/components/schemas/MemoryResponse' + architecture: + type: string + description: CPU architecture. + example: amd64 + kernel_version: + type: string + description: OS kernel version. + example: 5.15.0-91-generic + cpu_count: + type: integer + description: Number of logical CPUs. + example: 4 + fqdn: + type: string + description: Fully qualified domain name. + example: web-01.example.com + service_mgr: + type: string + description: Init system. + example: systemd + package_mgr: + type: string + description: Package manager. + example: apt + interfaces: + type: array + items: + $ref: '#/components/schemas/NetworkInterfaceResponse' + facts: + type: object + additionalProperties: true + description: Extended facts from additional providers. required: - hostname - status @@ -1406,6 +1438,31 @@ components: required: - distribution - version + NetworkInterfaceResponse: + type: object + properties: + name: + type: string + example: eth0 + ipv4: + type: string + example: 192.168.1.10 + ipv6: + type: string + example: fe80::1 + mac: + type: string + example: '00:11:22:33:44:55' + family: + type: string + description: IP address family. + example: dual + enum: + - inet + - inet6 + - dual + required: + - name AuditEntry: type: object properties: diff --git a/pkg/osapi/gen/client.gen.go b/pkg/osapi/gen/client.gen.go index 6036950..5780c87 100644 --- a/pkg/osapi/gen/client.gen.go +++ b/pkg/osapi/gen/client.gen.go @@ -34,6 +34,13 @@ const ( DNSUpdateResultItemStatusOk DNSUpdateResultItemStatus = "ok" ) +// Defines values for NetworkInterfaceResponseFamily. +const ( + Dual NetworkInterfaceResponseFamily = "dual" + Inet NetworkInterfaceResponseFamily = "inet" + Inet6 NetworkInterfaceResponseFamily = "inet6" +) + // Defines values for GetJobParamsStatus. const ( GetJobParamsStatusCompleted GetJobParamsStatus = "completed" @@ -57,8 +64,24 @@ type AgentDetail struct { // AgentInfo defines model for AgentInfo. type AgentInfo struct { + // Architecture CPU architecture. + Architecture *string `json:"architecture,omitempty"` + + // CpuCount Number of logical CPUs. + CpuCount *int `json:"cpu_count,omitempty"` + + // Facts Extended facts from additional providers. + Facts *map[string]interface{} `json:"facts,omitempty"` + + // Fqdn Fully qualified domain name. + Fqdn *string `json:"fqdn,omitempty"` + // Hostname The hostname of the agent. - Hostname string `json:"hostname"` + Hostname string `json:"hostname"` + Interfaces *[]NetworkInterfaceResponse `json:"interfaces,omitempty"` + + // KernelVersion OS kernel version. + KernelVersion *string `json:"kernel_version,omitempty"` // Labels Key-value labels configured on the agent. Labels *map[string]string `json:"labels,omitempty"` @@ -72,9 +95,15 @@ type AgentInfo struct { // OsInfo Operating system information. OsInfo *OSInfoResponse `json:"os_info,omitempty"` + // PackageMgr Package manager. + PackageMgr *string `json:"package_mgr,omitempty"` + // RegisteredAt When the agent last refreshed its heartbeat. RegisteredAt *time.Time `json:"registered_at,omitempty"` + // ServiceMgr Init system. + ServiceMgr *string `json:"service_mgr,omitempty"` + // StartedAt When the agent process started. StartedAt *time.Time `json:"started_at,omitempty"` @@ -570,6 +599,19 @@ type NATSInfo struct { Version string `json:"version"` } +// NetworkInterfaceResponse defines model for NetworkInterfaceResponse. +type NetworkInterfaceResponse struct { + // Family IP address family. + Family *NetworkInterfaceResponseFamily `json:"family,omitempty"` + Ipv4 *string `json:"ipv4,omitempty"` + Ipv6 *string `json:"ipv6,omitempty"` + Mac *string `json:"mac,omitempty"` + Name string `json:"name"` +} + +// NetworkInterfaceResponseFamily IP address family. +type NetworkInterfaceResponseFamily string + // NodeStatusCollectionResponse defines model for NodeStatusCollectionResponse. type NodeStatusCollectionResponse struct { // JobId The job ID used to process this request. diff --git a/pkg/osapi/health.go b/pkg/osapi/health.go index f66447c..9f9b819 100644 --- a/pkg/osapi/health.go +++ b/pkg/osapi/health.go @@ -22,6 +22,7 @@ package osapi import ( "context" + "fmt" "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" ) @@ -34,22 +35,99 @@ type HealthService struct { // Liveness checks if the API server process is alive. func (s *HealthService) Liveness( ctx context.Context, -) (*gen.GetHealthResponse, error) { - return s.client.GetHealthWithResponse(ctx) +) (*Response[HealthStatus], error) { + resp, err := s.client.GetHealthWithResponse(ctx) + if err != nil { + return nil, fmt.Errorf("health liveness: %w", err) + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(healthStatusFromGen(resp.JSON200), resp.Body), nil } // Ready checks if the API server and its dependencies are ready to -// serve traffic. +// serve traffic. A 503 response is treated as success with the +// ServiceUnavailable flag set. func (s *HealthService) Ready( ctx context.Context, -) (*gen.GetHealthReadyResponse, error) { - return s.client.GetHealthReadyWithResponse(ctx) +) (*Response[ReadyStatus], error) { + resp, err := s.client.GetHealthReadyWithResponse(ctx) + if err != nil { + return nil, fmt.Errorf("health ready: %w", err) + } + + switch resp.StatusCode() { + case 200: + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: 200, + Message: "nil response body", + }} + } + + return NewResponse(readyStatusFromGen(resp.JSON200, false), resp.Body), nil + case 503: + if resp.JSON503 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: 503, + Message: "nil response body", + }} + } + + return NewResponse(readyStatusFromGen(resp.JSON503, true), resp.Body), nil + default: + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "unexpected status", + }} + } } // Status returns detailed system status including component health, // NATS info, stream stats, and job queue counts. Requires authentication. +// A 503 response is treated as success with the ServiceUnavailable flag set. func (s *HealthService) Status( ctx context.Context, -) (*gen.GetHealthStatusResponse, error) { - return s.client.GetHealthStatusWithResponse(ctx) +) (*Response[SystemStatus], error) { + resp, err := s.client.GetHealthStatusWithResponse(ctx) + if err != nil { + return nil, fmt.Errorf("health status: %w", err) + } + + // Auth errors take precedence. + if resp.StatusCode() == 401 || resp.StatusCode() == 403 { + return nil, checkError(resp.StatusCode(), resp.JSON401, resp.JSON403) + } + + switch resp.StatusCode() { + case 200: + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: 200, + Message: "nil response body", + }} + } + + return NewResponse(systemStatusFromGen(resp.JSON200, false), resp.Body), nil + case 503: + if resp.JSON503 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: 503, + Message: "nil response body", + }} + } + + return NewResponse(systemStatusFromGen(resp.JSON503, true), resp.Body), nil + default: + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "unexpected status", + }} + } } diff --git a/pkg/osapi/health_public_test.go b/pkg/osapi/health_public_test.go index f35cd6e..aa784d7 100644 --- a/pkg/osapi/health_public_test.go +++ b/pkg/osapi/health_public_test.go @@ -22,6 +22,7 @@ package osapi_test import ( "context" + "errors" "log/slog" "net/http" "net/http/httptest" @@ -35,50 +36,77 @@ import ( type HealthPublicTestSuite struct { suite.Suite - ctx context.Context - server *httptest.Server - sut *osapi.Client + ctx context.Context } func (suite *HealthPublicTestSuite) SetupTest() { suite.ctx = context.Background() - - suite.server = httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{}`)) - }), - ) - - suite.sut = osapi.New( - suite.server.URL, - "test-token", - osapi.WithLogger(slog.Default()), - ) -} - -func (suite *HealthPublicTestSuite) TearDownTest() { - suite.server.Close() } func (suite *HealthPublicTestSuite) TestLiveness() { tests := []struct { name string - validateFunc func(error) + handler http.HandlerFunc + serverURL string + validateFunc func(*osapi.Response[osapi.HealthStatus], error) }{ { - name: "when checking liveness returns no error", - validateFunc: func(err error) { + name: "when checking liveness returns health status", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status":"ok"}`)) + }), + validateFunc: func(resp *osapi.Response[osapi.HealthStatus], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal("ok", resp.Data.Status) + }, + }, + { + name: "when client HTTP request fails returns error", + serverURL: "http://127.0.0.1:0", + validateFunc: func(resp *osapi.Response[osapi.HealthStatus], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "health liveness") + }, + }, + { + name: "when response body is nil returns UnexpectedStatusError", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + }), + validateFunc: func(resp *osapi.Response[osapi.HealthStatus], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Contains(target.Message, "nil response body") }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Health.Liveness(suite.ctx) - tc.validateFunc(err) + serverURL := tc.serverURL + if tc.handler != nil { + server := httptest.NewServer(tc.handler) + defer server.Close() + serverURL = server.URL + } + + sut := osapi.New( + serverURL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Health.Liveness(suite.ctx) + tc.validateFunc(resp, err) }) } } @@ -86,20 +114,140 @@ func (suite *HealthPublicTestSuite) TestLiveness() { func (suite *HealthPublicTestSuite) TestReady() { tests := []struct { name string - validateFunc func(error) + handler http.HandlerFunc + serverURL string + validateFunc func(*osapi.Response[osapi.ReadyStatus], error) }{ { - name: "when checking readiness returns no error", - validateFunc: func(err error) { + name: "when checking readiness returns ready status", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status":"ready"}`)) + }), + validateFunc: func(resp *osapi.Response[osapi.ReadyStatus], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal("ready", resp.Data.Status) + suite.False(resp.Data.ServiceUnavailable) + }, + }, + { + name: "when client HTTP request fails returns error", + serverURL: "http://127.0.0.1:0", + validateFunc: func(resp *osapi.Response[osapi.ReadyStatus], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "health ready") + }, + }, + { + name: "when 200 response body is nil returns UnexpectedStatusError", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + }), + validateFunc: func(resp *osapi.Response[osapi.ReadyStatus], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Contains(target.Message, "nil response body") + }, + }, + { + name: "when unexpected status returns UnexpectedStatusError", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + }), + validateFunc: func(resp *osapi.Response[osapi.ReadyStatus], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusInternalServerError, target.StatusCode) + suite.Contains(target.Message, "unexpected status") }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Health.Ready(suite.ctx) - tc.validateFunc(err) + serverURL := tc.serverURL + if tc.handler != nil { + server := httptest.NewServer(tc.handler) + defer server.Close() + serverURL = server.URL + } + + sut := osapi.New( + serverURL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Health.Ready(suite.ctx) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *HealthPublicTestSuite) TestReady503() { + tests := []struct { + name string + handler http.HandlerFunc + validateFunc func(*osapi.Response[osapi.ReadyStatus], error) + }{ + { + name: "when server returns 503 returns ready status with ServiceUnavailable", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + _, _ = w.Write([]byte(`{"status":"not_ready","error":"nats down"}`)) + }), + validateFunc: func(resp *osapi.Response[osapi.ReadyStatus], err error) { + suite.NoError(err) + suite.NotNil(resp) + suite.Equal("not_ready", resp.Data.Status) + suite.Equal("nats down", resp.Data.Error) + suite.True(resp.Data.ServiceUnavailable) + }, + }, + { + name: "when 503 response body is nil returns UnexpectedStatusError", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusServiceUnavailable) + }), + validateFunc: func(resp *osapi.Response[osapi.ReadyStatus], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusServiceUnavailable, target.StatusCode) + suite.Contains(target.Message, "nil response body") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Health.Ready(suite.ctx) + tc.validateFunc(resp, err) }) } } @@ -107,20 +255,198 @@ func (suite *HealthPublicTestSuite) TestReady() { func (suite *HealthPublicTestSuite) TestStatus() { tests := []struct { name string - validateFunc func(error) + handler http.HandlerFunc + serverURL string + validateFunc func(*osapi.Response[osapi.SystemStatus], error) }{ { - name: "when checking status returns no error", - validateFunc: func(err error) { + name: "when checking status returns system status", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status":"ok","version":"1.0.0","uptime":"1h"}`)) + }), + validateFunc: func(resp *osapi.Response[osapi.SystemStatus], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal("ok", resp.Data.Status) + suite.Equal("1.0.0", resp.Data.Version) + suite.Equal("1h", resp.Data.Uptime) + suite.False(resp.Data.ServiceUnavailable) + }, + }, + { + name: "when client HTTP request fails returns error", + serverURL: "http://127.0.0.1:0", + validateFunc: func(resp *osapi.Response[osapi.SystemStatus], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "health status") + }, + }, + { + name: "when 200 response body is nil returns UnexpectedStatusError", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + }), + validateFunc: func(resp *osapi.Response[osapi.SystemStatus], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Contains(target.Message, "nil response body") + }, + }, + { + name: "when unexpected status returns UnexpectedStatusError", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusTeapot) + }), + validateFunc: func(resp *osapi.Response[osapi.SystemStatus], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusTeapot, target.StatusCode) + suite.Contains(target.Message, "unexpected status") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + serverURL := tc.serverURL + if tc.handler != nil { + server := httptest.NewServer(tc.handler) + defer server.Close() + serverURL = server.URL + } + + sut := osapi.New( + serverURL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Health.Status(suite.ctx) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *HealthPublicTestSuite) TestStatus503() { + tests := []struct { + name string + handler http.HandlerFunc + validateFunc func(*osapi.Response[osapi.SystemStatus], error) + }{ + { + name: "when server returns 503 returns status with ServiceUnavailable", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + _, _ = w.Write([]byte(`{"status":"degraded","version":"1.0.0","uptime":"1h"}`)) + }), + validateFunc: func(resp *osapi.Response[osapi.SystemStatus], err error) { + suite.NoError(err) + suite.NotNil(resp) + suite.Equal("degraded", resp.Data.Status) + suite.True(resp.Data.ServiceUnavailable) + }, + }, + { + name: "when 503 response body is nil returns UnexpectedStatusError", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusServiceUnavailable) + }), + validateFunc: func(resp *osapi.Response[osapi.SystemStatus], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusServiceUnavailable, target.StatusCode) + suite.Contains(target.Message, "nil response body") }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Health.Status(suite.ctx) - tc.validateFunc(err) + server := httptest.NewServer(tc.handler) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Health.Status(suite.ctx) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *HealthPublicTestSuite) TestStatusAuthError() { + tests := []struct { + name string + handler http.HandlerFunc + validateFunc func(*osapi.Response[osapi.SystemStatus], error) + }{ + { + name: "when server returns 401 returns AuthError", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + validateFunc: func(resp *osapi.Response[osapi.SystemStatus], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusUnauthorized, target.StatusCode) + }, + }, + { + name: "when server returns 403 returns AuthError", + handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + validateFunc: func(resp *osapi.Response[osapi.SystemStatus], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Health.Status(suite.ctx) + tc.validateFunc(resp, err) }) } } diff --git a/pkg/osapi/health_types.go b/pkg/osapi/health_types.go new file mode 100644 index 0000000..c1628a0 --- /dev/null +++ b/pkg/osapi/health_types.go @@ -0,0 +1,267 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" + +// HealthStatus represents a liveness check response. +type HealthStatus struct { + Status string +} + +// ReadyStatus represents a readiness check response. +type ReadyStatus struct { + Status string + Error string + ServiceUnavailable bool +} + +// SystemStatus represents detailed system status. +type SystemStatus struct { + Status string + Version string + Uptime string + ServiceUnavailable bool + Components map[string]ComponentHealth + NATS *NATSInfo + Agents *AgentStats + Jobs *JobStats + Consumers *ConsumerStats + Streams []StreamInfo + KVBuckets []KVBucketInfo +} + +// ComponentHealth represents a component's health. +type ComponentHealth struct { + Status string + Error string +} + +// NATSInfo represents NATS connection info. +type NATSInfo struct { + URL string + Version string +} + +// AgentStats represents agent statistics from the health endpoint. +type AgentStats struct { + Total int + Ready int + Agents []AgentSummary +} + +// AgentSummary represents a summary of an agent from the health endpoint. +type AgentSummary struct { + Hostname string + Labels string + Registered string +} + +// JobStats represents job queue statistics from the health endpoint. +type JobStats struct { + Total int + Completed int + Failed int + Processing int + Unprocessed int + Dlq int +} + +// ConsumerStats represents JetStream consumer statistics. +type ConsumerStats struct { + Total int + Consumers []ConsumerDetail +} + +// ConsumerDetail represents a single consumer's details. +type ConsumerDetail struct { + Name string + Pending int + AckPending int + Redelivered int +} + +// StreamInfo represents a JetStream stream's info. +type StreamInfo struct { + Name string + Messages int + Bytes int + Consumers int +} + +// KVBucketInfo represents a KV bucket's info. +type KVBucketInfo struct { + Name string + Keys int + Bytes int +} + +// healthStatusFromGen converts a gen.HealthResponse to a HealthStatus. +func healthStatusFromGen( + g *gen.HealthResponse, +) HealthStatus { + return HealthStatus{ + Status: g.Status, + } +} + +// readyStatusFromGen converts a gen.ReadyResponse to a ReadyStatus. +func readyStatusFromGen( + g *gen.ReadyResponse, + serviceUnavailable bool, +) ReadyStatus { + r := ReadyStatus{ + Status: g.Status, + ServiceUnavailable: serviceUnavailable, + } + + if g.Error != nil { + r.Error = *g.Error + } + + return r +} + +// systemStatusFromGen converts a gen.StatusResponse to a SystemStatus. +func systemStatusFromGen( + g *gen.StatusResponse, + serviceUnavailable bool, +) SystemStatus { + s := SystemStatus{ + Status: g.Status, + Version: g.Version, + Uptime: g.Uptime, + ServiceUnavailable: serviceUnavailable, + } + + if g.Components != nil { + comps := make(map[string]ComponentHealth, len(g.Components)) + for k, v := range g.Components { + ch := ComponentHealth{ + Status: v.Status, + } + + if v.Error != nil { + ch.Error = *v.Error + } + + comps[k] = ch + } + + s.Components = comps + } + + if g.Nats != nil { + s.NATS = &NATSInfo{ + URL: g.Nats.Url, + Version: g.Nats.Version, + } + } + + if g.Agents != nil { + as := &AgentStats{ + Total: g.Agents.Total, + Ready: g.Agents.Ready, + } + + if g.Agents.Agents != nil { + agents := make([]AgentSummary, 0, len(*g.Agents.Agents)) + for _, a := range *g.Agents.Agents { + summary := AgentSummary{ + Hostname: a.Hostname, + Registered: a.Registered, + } + + if a.Labels != nil { + summary.Labels = *a.Labels + } + + agents = append(agents, summary) + } + + as.Agents = agents + } + + s.Agents = as + } + + if g.Jobs != nil { + s.Jobs = &JobStats{ + Total: g.Jobs.Total, + Completed: g.Jobs.Completed, + Failed: g.Jobs.Failed, + Processing: g.Jobs.Processing, + Unprocessed: g.Jobs.Unprocessed, + Dlq: g.Jobs.Dlq, + } + } + + if g.Consumers != nil { + cs := &ConsumerStats{ + Total: g.Consumers.Total, + } + + if g.Consumers.Consumers != nil { + consumers := make([]ConsumerDetail, 0, len(*g.Consumers.Consumers)) + for _, c := range *g.Consumers.Consumers { + consumers = append(consumers, ConsumerDetail{ + Name: c.Name, + Pending: c.Pending, + AckPending: c.AckPending, + Redelivered: c.Redelivered, + }) + } + + cs.Consumers = consumers + } + + s.Consumers = cs + } + + if g.Streams != nil { + streams := make([]StreamInfo, 0, len(*g.Streams)) + for _, st := range *g.Streams { + streams = append(streams, StreamInfo{ + Name: st.Name, + Messages: st.Messages, + Bytes: st.Bytes, + Consumers: st.Consumers, + }) + } + + s.Streams = streams + } + + if g.KvBuckets != nil { + buckets := make([]KVBucketInfo, 0, len(*g.KvBuckets)) + for _, b := range *g.KvBuckets { + buckets = append(buckets, KVBucketInfo{ + Name: b.Name, + Keys: b.Keys, + Bytes: b.Bytes, + }) + } + + s.KVBuckets = buckets + } + + return s +} diff --git a/pkg/osapi/health_types_test.go b/pkg/osapi/health_types_test.go new file mode 100644 index 0000000..e9ebe88 --- /dev/null +++ b/pkg/osapi/health_types_test.go @@ -0,0 +1,340 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +type HealthTypesTestSuite struct { + suite.Suite +} + +func (suite *HealthTypesTestSuite) TestHealthStatusFromGen() { + tests := []struct { + name string + input *gen.HealthResponse + validateFunc func(HealthStatus) + }{ + { + name: "when status is ok", + input: &gen.HealthResponse{ + Status: "ok", + }, + validateFunc: func(h HealthStatus) { + suite.Equal("ok", h.Status) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := healthStatusFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *HealthTypesTestSuite) TestReadyStatusFromGen() { + tests := []struct { + name string + input *gen.ReadyResponse + serviceUnavailable bool + validateFunc func(ReadyStatus) + }{ + { + name: "when ready with no error", + input: &gen.ReadyResponse{ + Status: "ready", + }, + serviceUnavailable: false, + validateFunc: func(r ReadyStatus) { + suite.Equal("ready", r.Status) + suite.Empty(r.Error) + suite.False(r.ServiceUnavailable) + }, + }, + { + name: "when not ready with error", + input: func() *gen.ReadyResponse { + errMsg := "NATS connection failed" + + return &gen.ReadyResponse{ + Status: "not_ready", + Error: &errMsg, + } + }(), + serviceUnavailable: true, + validateFunc: func(r ReadyStatus) { + suite.Equal("not_ready", r.Status) + suite.Equal("NATS connection failed", r.Error) + suite.True(r.ServiceUnavailable) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := readyStatusFromGen(tc.input, tc.serviceUnavailable) + tc.validateFunc(result) + }) + } +} + +func (suite *HealthTypesTestSuite) TestSystemStatusFromGen() { + tests := []struct { + name string + input *gen.StatusResponse + serviceUnavailable bool + validateFunc func(SystemStatus) + }{ + { + name: "when all fields are populated", + input: func() *gen.StatusResponse { + errMsg := "connection timeout" + labels := "group=web" + + return &gen.StatusResponse{ + Status: "degraded", + Version: "1.2.3", + Uptime: "5d 3h", + Components: map[string]gen.ComponentHealth{ + "nats": { + Status: "healthy", + }, + "store": { + Status: "unhealthy", + Error: &errMsg, + }, + }, + Nats: &gen.NATSInfo{ + Url: "nats://localhost:4222", + Version: "2.10.0", + }, + Agents: &gen.AgentStats{ + Total: 3, + Ready: 2, + Agents: &[]gen.AgentDetail{ + { + Hostname: "web-01", + Labels: &labels, + Registered: "5m ago", + }, + { + Hostname: "web-02", + Registered: "10m ago", + }, + }, + }, + Jobs: &gen.JobStats{ + Total: 100, + Completed: 80, + Failed: 5, + Processing: 10, + Unprocessed: 3, + Dlq: 2, + }, + Consumers: &gen.ConsumerStats{ + Total: 2, + Consumers: &[]gen.ConsumerDetail{ + { + Name: "jobs-agent", + Pending: 5, + AckPending: 2, + Redelivered: 1, + }, + }, + }, + Streams: &[]gen.StreamInfo{ + { + Name: "JOBS", + Messages: 500, + Bytes: 1048576, + Consumers: 2, + }, + }, + KvBuckets: &[]gen.KVBucketInfo{ + { + Name: "job-queue", + Keys: 50, + Bytes: 524288, + }, + { + Name: "audit-log", + Keys: 200, + Bytes: 2097152, + }, + }, + } + }(), + serviceUnavailable: false, + validateFunc: func(s SystemStatus) { + suite.Equal("degraded", s.Status) + suite.Equal("1.2.3", s.Version) + suite.Equal("5d 3h", s.Uptime) + suite.False(s.ServiceUnavailable) + + suite.Require().Len(s.Components, 2) + suite.Equal("healthy", s.Components["nats"].Status) + suite.Empty(s.Components["nats"].Error) + suite.Equal("unhealthy", s.Components["store"].Status) + suite.Equal("connection timeout", s.Components["store"].Error) + + suite.Require().NotNil(s.NATS) + suite.Equal("nats://localhost:4222", s.NATS.URL) + suite.Equal("2.10.0", s.NATS.Version) + + suite.Require().NotNil(s.Agents) + suite.Equal(3, s.Agents.Total) + suite.Equal(2, s.Agents.Ready) + suite.Require().Len(s.Agents.Agents, 2) + suite.Equal("web-01", s.Agents.Agents[0].Hostname) + suite.Equal("group=web", s.Agents.Agents[0].Labels) + suite.Equal("5m ago", s.Agents.Agents[0].Registered) + suite.Equal("web-02", s.Agents.Agents[1].Hostname) + suite.Empty(s.Agents.Agents[1].Labels) + suite.Equal("10m ago", s.Agents.Agents[1].Registered) + + suite.Require().NotNil(s.Jobs) + suite.Equal(100, s.Jobs.Total) + suite.Equal(80, s.Jobs.Completed) + suite.Equal(5, s.Jobs.Failed) + suite.Equal(10, s.Jobs.Processing) + suite.Equal(3, s.Jobs.Unprocessed) + suite.Equal(2, s.Jobs.Dlq) + + suite.Require().NotNil(s.Consumers) + suite.Equal(2, s.Consumers.Total) + suite.Require().Len(s.Consumers.Consumers, 1) + suite.Equal("jobs-agent", s.Consumers.Consumers[0].Name) + suite.Equal(5, s.Consumers.Consumers[0].Pending) + suite.Equal(2, s.Consumers.Consumers[0].AckPending) + suite.Equal(1, s.Consumers.Consumers[0].Redelivered) + + suite.Require().Len(s.Streams, 1) + suite.Equal("JOBS", s.Streams[0].Name) + suite.Equal(500, s.Streams[0].Messages) + suite.Equal(1048576, s.Streams[0].Bytes) + suite.Equal(2, s.Streams[0].Consumers) + + suite.Require().Len(s.KVBuckets, 2) + suite.Equal("job-queue", s.KVBuckets[0].Name) + suite.Equal(50, s.KVBuckets[0].Keys) + suite.Equal(524288, s.KVBuckets[0].Bytes) + suite.Equal("audit-log", s.KVBuckets[1].Name) + suite.Equal(200, s.KVBuckets[1].Keys) + suite.Equal(2097152, s.KVBuckets[1].Bytes) + }, + }, + { + name: "when only required fields are set", + input: &gen.StatusResponse{ + Status: "ok", + Version: "1.0.0", + Uptime: "1h", + Components: map[string]gen.ComponentHealth{}, + }, + serviceUnavailable: false, + validateFunc: func(s SystemStatus) { + suite.Equal("ok", s.Status) + suite.Equal("1.0.0", s.Version) + suite.Equal("1h", s.Uptime) + suite.False(s.ServiceUnavailable) + suite.Empty(s.Components) + suite.Nil(s.NATS) + suite.Nil(s.Agents) + suite.Nil(s.Jobs) + suite.Nil(s.Consumers) + suite.Nil(s.Streams) + suite.Nil(s.KVBuckets) + }, + }, + { + name: "when service unavailable is true", + input: &gen.StatusResponse{ + Status: "degraded", + Version: "1.0.0", + Uptime: "30m", + Components: map[string]gen.ComponentHealth{}, + }, + serviceUnavailable: true, + validateFunc: func(s SystemStatus) { + suite.Equal("degraded", s.Status) + suite.True(s.ServiceUnavailable) + }, + }, + { + name: "when agents has nil agents list", + input: &gen.StatusResponse{ + Status: "ok", + Version: "1.0.0", + Uptime: "1h", + Components: map[string]gen.ComponentHealth{ + "nats": {Status: "healthy"}, + }, + Agents: &gen.AgentStats{ + Total: 0, + Ready: 0, + }, + }, + serviceUnavailable: false, + validateFunc: func(s SystemStatus) { + suite.Require().NotNil(s.Agents) + suite.Equal(0, s.Agents.Total) + suite.Equal(0, s.Agents.Ready) + suite.Nil(s.Agents.Agents) + }, + }, + { + name: "when consumers has nil consumers list", + input: &gen.StatusResponse{ + Status: "ok", + Version: "1.0.0", + Uptime: "1h", + Components: map[string]gen.ComponentHealth{}, + Consumers: &gen.ConsumerStats{ + Total: 0, + }, + }, + serviceUnavailable: false, + validateFunc: func(s SystemStatus) { + suite.Require().NotNil(s.Consumers) + suite.Equal(0, s.Consumers.Total) + suite.Nil(s.Consumers.Consumers) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := systemStatusFromGen(tc.input, tc.serviceUnavailable) + tc.validateFunc(result) + }) + } +} + +func TestHealthTypesTestSuite(t *testing.T) { + suite.Run(t, new(HealthTypesTestSuite)) +} diff --git a/pkg/osapi/job.go b/pkg/osapi/job.go index 8be659b..2ae3849 100644 --- a/pkg/osapi/job.go +++ b/pkg/osapi/job.go @@ -39,39 +39,80 @@ func (s *JobService) Create( ctx context.Context, operation map[string]interface{}, target string, -) (*gen.PostJobResponse, error) { +) (*Response[JobCreated], error) { body := gen.CreateJobRequest{ Operation: operation, TargetHostname: target, } - return s.client.PostJobWithResponse(ctx, body) + resp, err := s.client.PostJobWithResponse(ctx, body) + if err != nil { + return nil, fmt.Errorf("create job: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON201 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(jobCreatedFromGen(resp.JSON201), resp.Body), nil } // Get retrieves a job by ID. func (s *JobService) Get( ctx context.Context, id string, -) (*gen.GetJobByIDResponse, error) { +) (*Response[JobDetail], error) { parsedID, err := uuid.Parse(id) if err != nil { return nil, fmt.Errorf("invalid job ID: %w", err) } - return s.client.GetJobByIDWithResponse(ctx, parsedID) + resp, err := s.client.GetJobByIDWithResponse(ctx, parsedID) + if err != nil { + return nil, fmt.Errorf("get job: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(jobDetailFromGen(resp.JSON200), resp.Body), nil } // Delete deletes a job by ID. func (s *JobService) Delete( ctx context.Context, id string, -) (*gen.DeleteJobByIDResponse, error) { +) error { parsedID, err := uuid.Parse(id) if err != nil { - return nil, fmt.Errorf("invalid job ID: %w", err) + return fmt.Errorf("invalid job ID: %w", err) + } + + resp, err := s.client.DeleteJobByIDWithResponse(ctx, parsedID) + if err != nil { + return fmt.Errorf("delete job: %w", err) } - return s.client.DeleteJobByIDWithResponse(ctx, parsedID) + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON500); err != nil { + return err + } + + return nil } // ListParams contains optional filters for listing jobs. @@ -91,7 +132,7 @@ type ListParams struct { func (s *JobService) List( ctx context.Context, params ListParams, -) (*gen.GetJobResponse, error) { +) (*Response[JobList], error) { p := &gen.GetJobParams{} if params.Status != "" { @@ -107,14 +148,46 @@ func (s *JobService) List( p.Offset = ¶ms.Offset } - return s.client.GetJobWithResponse(ctx, p) + resp, err := s.client.GetJobWithResponse(ctx, p) + if err != nil { + return nil, fmt.Errorf("list jobs: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(jobListFromGen(resp.JSON200), resp.Body), nil } // QueueStats retrieves job queue statistics. func (s *JobService) QueueStats( ctx context.Context, -) (*gen.GetJobStatusResponse, error) { - return s.client.GetJobStatusWithResponse(ctx) +) (*Response[QueueStats], error) { + resp, err := s.client.GetJobStatusWithResponse(ctx) + if err != nil { + return nil, fmt.Errorf("queue stats: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(queueStatsFromGen(resp.JSON200), resp.Body), nil } // Retry retries a failed job by ID, optionally on a different target. @@ -122,7 +195,7 @@ func (s *JobService) Retry( ctx context.Context, id string, target string, -) (*gen.RetryJobByIDResponse, error) { +) (*Response[JobCreated], error) { parsedID, err := uuid.Parse(id) if err != nil { return nil, fmt.Errorf("invalid job ID: %w", err) @@ -133,5 +206,21 @@ func (s *JobService) Retry( body.TargetHostname = &target } - return s.client.RetryJobByIDWithResponse(ctx, parsedID, body) + resp, err := s.client.RetryJobByIDWithResponse(ctx, parsedID, body) + if err != nil { + return nil, fmt.Errorf("retry job: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON201 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(jobCreatedFromGen(resp.JSON201), resp.Body), nil } diff --git a/pkg/osapi/job_public_test.go b/pkg/osapi/job_public_test.go index c73c7f5..0722079 100644 --- a/pkg/osapi/job_public_test.go +++ b/pkg/osapi/job_public_test.go @@ -22,6 +22,7 @@ package osapi_test import ( "context" + "errors" "log/slog" "net/http" "net/http/httptest" @@ -35,31 +36,11 @@ import ( type JobPublicTestSuite struct { suite.Suite - ctx context.Context - server *httptest.Server - sut *osapi.Client + ctx context.Context } func (suite *JobPublicTestSuite) SetupTest() { suite.ctx = context.Background() - - suite.server = httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{}`)) - }), - ) - - suite.sut = osapi.New( - suite.server.URL, - "test-token", - osapi.WithLogger(slog.Default()), - ) -} - -func (suite *JobPublicTestSuite) TearDownTest() { - suite.server.Close() } func (suite *JobPublicTestSuite) TestCreate() { @@ -67,22 +48,158 @@ func (suite *JobPublicTestSuite) TestCreate() { name string operation map[string]interface{} target string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.JobCreated], error) }{ { - name: "when creating job returns no error", + name: "when creating job returns response", operation: map[string]interface{}{"type": "system.hostname.get"}, target: "_any", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal("550e8400-e29b-41d4-a716-446655440000", resp.Data.JobID) + suite.Equal("pending", resp.Data.Status) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Job.Create(suite.ctx, tc.operation, tc.target) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, _ = w.Write( + []byte( + `{"job_id":"550e8400-e29b-41d4-a716-446655440000","status":"pending"}`, + ), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Create(suite.ctx, tc.operation, tc.target) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestCreateError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobCreated], error) + }{ + { + name: "when server returns 400 returns ValidationError", + validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.ValidationError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusBadRequest, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"error":"validation failed"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Create(suite.ctx, map[string]interface{}{}, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestCreateHTTPError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobCreated], error) + }{ + { + name: "when HTTP request fails returns error", + validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "create job") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Create(suite.ctx, map[string]interface{}{}, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestCreateNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobCreated], error) + }{ + { + name: "when server returns 201 with empty body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Contains(target.Message, "nil response body") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusCreated) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Create(suite.ctx, map[string]interface{}{}, "_any") + tc.validateFunc(resp, err) }) } } @@ -91,20 +208,24 @@ func (suite *JobPublicTestSuite) TestGet() { tests := []struct { name string id string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.JobDetail], error) }{ { - name: "when valid UUID returns no error", + name: "when valid UUID returns response", id: "550e8400-e29b-41d4-a716-446655440000", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.JobDetail], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal("550e8400-e29b-41d4-a716-446655440000", resp.Data.ID) + suite.Equal("completed", resp.Data.Status) }, }, { name: "when invalid UUID returns error", id: "not-a-uuid", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.JobDetail], err error) { suite.Error(err) + suite.Nil(resp) suite.Contains(err.Error(), "invalid job ID") }, }, @@ -112,8 +233,142 @@ func (suite *JobPublicTestSuite) TestGet() { for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Job.Get(suite.ctx, tc.id) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"id":"550e8400-e29b-41d4-a716-446655440000","status":"completed"}`, + ), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Get(suite.ctx, tc.id) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestGetHTTPError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobDetail], error) + }{ + { + name: "when HTTP request fails returns error", + validateFunc: func(resp *osapi.Response[osapi.JobDetail], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get job") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Get(suite.ctx, "00000000-0000-0000-0000-000000000000") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestGetNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobDetail], error) + }{ + { + name: "when server returns 200 with empty body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.JobDetail], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Contains(target.Message, "nil response body") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Get(suite.ctx, "00000000-0000-0000-0000-000000000000") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestGetNotFound() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobDetail], error) + }{ + { + name: "when server returns 404 returns NotFoundError", + validateFunc: func(resp *osapi.Response[osapi.JobDetail], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.NotFoundError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusNotFound, target.StatusCode) + suite.Equal("job not found", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"error":"job not found"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Get(suite.ctx, "550e8400-e29b-41d4-a716-446655440000") + tc.validateFunc(resp, err) }) } } @@ -143,7 +398,94 @@ func (suite *JobPublicTestSuite) TestDelete() { for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Job.Delete(suite.ctx, tc.id) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + err := sut.Job.Delete(suite.ctx, tc.id) + tc.validateFunc(err) + }) + } +} + +func (suite *JobPublicTestSuite) TestDeleteHTTPError() { + tests := []struct { + name string + validateFunc func(error) + }{ + { + name: "when HTTP request fails returns error", + validateFunc: func(err error) { + suite.Error(err) + suite.Contains(err.Error(), "delete job") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + err := sut.Job.Delete(suite.ctx, "00000000-0000-0000-0000-000000000000") + tc.validateFunc(err) + }) + } +} + +func (suite *JobPublicTestSuite) TestDeleteNotFound() { + tests := []struct { + name string + validateFunc func(error) + }{ + { + name: "when server returns 404 returns NotFoundError", + validateFunc: func(err error) { + suite.Error(err) + + var target *osapi.NotFoundError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusNotFound, target.StatusCode) + suite.Equal("job not found", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"error":"job not found"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + err := sut.Job.Delete(suite.ctx, "550e8400-e29b-41d4-a716-446655440000") tc.validateFunc(err) }) } @@ -153,32 +495,165 @@ func (suite *JobPublicTestSuite) TestList() { tests := []struct { name string params osapi.ListParams - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.JobList], error) }{ { - name: "when no filters returns no error", + name: "when no filters returns response", params: osapi.ListParams{}, - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.JobList], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal(0, resp.Data.TotalItems) + suite.Empty(resp.Data.Items) }, }, { - name: "when all filters provided returns no error", + name: "when all filters provided returns response", params: osapi.ListParams{ Status: "completed", Limit: 10, Offset: 5, }, - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.JobList], err error) { suite.NoError(err) + suite.NotNil(resp) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Job.List(suite.ctx, tc.params) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"items":[],"total_items":0}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.List(suite.ctx, tc.params) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestListHTTPError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobList], error) + }{ + { + name: "when HTTP request fails returns error", + validateFunc: func(resp *osapi.Response[osapi.JobList], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "list jobs") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.List(suite.ctx, osapi.ListParams{}) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestListError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobList], error) + }{ + { + name: "when server returns 401 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.JobList], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusUnauthorized, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.List(suite.ctx, osapi.ListParams{}) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestListNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobList], error) + }{ + { + name: "when server returns 200 with empty body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.JobList], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Contains(target.Message, "nil response body") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.List(suite.ctx, osapi.ListParams{}) + tc.validateFunc(resp, err) }) } } @@ -186,20 +661,151 @@ func (suite *JobPublicTestSuite) TestList() { func (suite *JobPublicTestSuite) TestQueueStats() { tests := []struct { name string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.QueueStats], error) }{ { - name: "when requesting queue stats returns no error", - validateFunc: func(err error) { + name: "when requesting queue stats returns response", + validateFunc: func(resp *osapi.Response[osapi.QueueStats], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal(5, resp.Data.TotalJobs) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Job.QueueStats(suite.ctx) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"total_jobs":5}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.QueueStats(suite.ctx) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestQueueStatsHTTPError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.QueueStats], error) + }{ + { + name: "when HTTP request fails returns error", + validateFunc: func(resp *osapi.Response[osapi.QueueStats], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "queue stats") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.QueueStats(suite.ctx) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestQueueStatsError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.QueueStats], error) + }{ + { + name: "when server returns 401 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.QueueStats], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusUnauthorized, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.QueueStats(suite.ctx) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestQueueStatsNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.QueueStats], error) + }{ + { + name: "when server returns 200 with empty body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.QueueStats], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Contains(target.Message, "nil response body") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.QueueStats(suite.ctx) + tc.validateFunc(resp, err) }) } } @@ -209,30 +815,35 @@ func (suite *JobPublicTestSuite) TestRetry() { name string id string target string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.JobCreated], error) }{ { - name: "when valid UUID with empty target returns no error", + name: "when valid UUID with empty target returns response", id: "550e8400-e29b-41d4-a716-446655440000", target: "", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal("550e8400-e29b-41d4-a716-446655440000", resp.Data.JobID) + suite.Equal("pending", resp.Data.Status) }, }, { - name: "when valid UUID with target returns no error", + name: "when valid UUID with target returns response", id: "550e8400-e29b-41d4-a716-446655440000", target: "web-01", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) { suite.NoError(err) + suite.NotNil(resp) }, }, { name: "when invalid UUID returns error", id: "not-a-uuid", target: "", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) { suite.Error(err) + suite.Nil(resp) suite.Contains(err.Error(), "invalid job ID") }, }, @@ -240,8 +851,153 @@ func (suite *JobPublicTestSuite) TestRetry() { for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Job.Retry(suite.ctx, tc.id, tc.target) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, _ = w.Write( + []byte( + `{"job_id":"550e8400-e29b-41d4-a716-446655440000","status":"pending"}`, + ), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Retry(suite.ctx, tc.id, tc.target) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestRetryHTTPError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobCreated], error) + }{ + { + name: "when HTTP request fails returns error", + validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "retry job") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Retry( + suite.ctx, + "00000000-0000-0000-0000-000000000000", + "", + ) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestRetryError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobCreated], error) + }{ + { + name: "when server returns 404 returns NotFoundError", + validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.NotFoundError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusNotFound, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"error":"job not found"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Retry( + suite.ctx, + "00000000-0000-0000-0000-000000000000", + "", + ) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *JobPublicTestSuite) TestRetryNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.JobCreated], error) + }{ + { + name: "when server returns 201 with empty body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Contains(target.Message, "nil response body") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusCreated) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Job.Retry( + suite.ctx, + "00000000-0000-0000-0000-000000000000", + "", + ) + tc.validateFunc(resp, err) }) } } diff --git a/pkg/osapi/job_types.go b/pkg/osapi/job_types.go new file mode 100644 index 0000000..ccd30fc --- /dev/null +++ b/pkg/osapi/job_types.go @@ -0,0 +1,271 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +// JobCreated represents a newly created job response. +type JobCreated struct { + JobID string + Status string + Revision int64 + Timestamp string +} + +// JobDetail represents a job's full details. +type JobDetail struct { + ID string + Status string + Hostname string + Created string + UpdatedAt string + Error string + Operation map[string]any + Result any + AgentStates map[string]AgentState + Responses map[string]AgentJobResponse + Timeline []TimelineEvent +} + +// AgentState represents an agent's processing state for a broadcast job. +type AgentState struct { + Status string + Duration string + Error string +} + +// AgentJobResponse represents an agent's response data for a broadcast job. +type AgentJobResponse struct { + Hostname string + Status string + Error string + Data any +} + +// TimelineEvent represents a job lifecycle event. +type TimelineEvent struct { + Timestamp string + Event string + Hostname string + Message string + Error string +} + +// JobList is a paginated list of jobs. +type JobList struct { + Items []JobDetail + TotalItems int +} + +// QueueStats represents job queue statistics. +type QueueStats struct { + TotalJobs int + DlqCount int + StatusCounts map[string]int + OperationCounts map[string]int +} + +// jobCreatedFromGen converts a gen.CreateJobResponse to a JobCreated. +func jobCreatedFromGen( + g *gen.CreateJobResponse, +) JobCreated { + j := JobCreated{ + JobID: g.JobId.String(), + Status: g.Status, + } + + if g.Revision != nil { + j.Revision = *g.Revision + } + + if g.Timestamp != nil { + j.Timestamp = *g.Timestamp + } + + return j +} + +// jobDetailFromGen converts a gen.JobDetailResponse to a JobDetail. +func jobDetailFromGen( + g *gen.JobDetailResponse, +) JobDetail { + j := JobDetail{} + + if g.Id != nil { + j.ID = g.Id.String() + } + + if g.Status != nil { + j.Status = *g.Status + } + + if g.Hostname != nil { + j.Hostname = *g.Hostname + } + + if g.Created != nil { + j.Created = *g.Created + } + + if g.UpdatedAt != nil { + j.UpdatedAt = *g.UpdatedAt + } + + if g.Error != nil { + j.Error = *g.Error + } + + if g.Operation != nil { + j.Operation = *g.Operation + } + + j.Result = g.Result + + if g.AgentStates != nil { + states := make(map[string]AgentState, len(*g.AgentStates)) + for k, v := range *g.AgentStates { + as := AgentState{} + + if v.Status != nil { + as.Status = *v.Status + } + + if v.Duration != nil { + as.Duration = *v.Duration + } + + if v.Error != nil { + as.Error = *v.Error + } + + states[k] = as + } + + j.AgentStates = states + } + + if g.Responses != nil { + responses := make(map[string]AgentJobResponse, len(*g.Responses)) + for k, v := range *g.Responses { + r := AgentJobResponse{ + Data: v.Data, + } + + if v.Hostname != nil { + r.Hostname = *v.Hostname + } + + if v.Status != nil { + r.Status = *v.Status + } + + if v.Error != nil { + r.Error = *v.Error + } + + responses[k] = r + } + + j.Responses = responses + } + + if g.Timeline != nil { + timeline := make([]TimelineEvent, 0, len(*g.Timeline)) + for _, v := range *g.Timeline { + te := TimelineEvent{} + + if v.Timestamp != nil { + te.Timestamp = *v.Timestamp + } + + if v.Event != nil { + te.Event = *v.Event + } + + if v.Hostname != nil { + te.Hostname = *v.Hostname + } + + if v.Message != nil { + te.Message = *v.Message + } + + if v.Error != nil { + te.Error = *v.Error + } + + timeline = append(timeline, te) + } + + j.Timeline = timeline + } + + return j +} + +// jobListFromGen converts a gen.ListJobsResponse to a JobList. +func jobListFromGen( + g *gen.ListJobsResponse, +) JobList { + jl := JobList{} + + if g.TotalItems != nil { + jl.TotalItems = *g.TotalItems + } + + if g.Items != nil { + items := make([]JobDetail, 0, len(*g.Items)) + for i := range *g.Items { + items = append(items, jobDetailFromGen(&(*g.Items)[i])) + } + + jl.Items = items + } + + return jl +} + +// queueStatsFromGen converts a gen.QueueStatsResponse to QueueStats. +func queueStatsFromGen( + g *gen.QueueStatsResponse, +) QueueStats { + qs := QueueStats{} + + if g.TotalJobs != nil { + qs.TotalJobs = *g.TotalJobs + } + + if g.DlqCount != nil { + qs.DlqCount = *g.DlqCount + } + + if g.StatusCounts != nil { + qs.StatusCounts = *g.StatusCounts + } + + if g.OperationCounts != nil { + qs.OperationCounts = *g.OperationCounts + } + + return qs +} diff --git a/pkg/osapi/job_types_test.go b/pkg/osapi/job_types_test.go new file mode 100644 index 0000000..f446d71 --- /dev/null +++ b/pkg/osapi/job_types_test.go @@ -0,0 +1,361 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + "testing" + + "github.com/google/uuid" + openapi_types "github.com/oapi-codegen/runtime/types" + "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +type JobTypesTestSuite struct { + suite.Suite +} + +func (suite *JobTypesTestSuite) TestJobCreatedFromGen() { + tests := []struct { + name string + input *gen.CreateJobResponse + validateFunc func(JobCreated) + }{ + { + name: "when all fields are populated", + input: func() *gen.CreateJobResponse { + rev := int64(42) + ts := "2026-03-04T12:00:00Z" + return &gen.CreateJobResponse{ + JobId: openapi_types.UUID( + uuid.MustParse("11111111-1111-1111-1111-111111111111"), + ), + Status: "pending", + Revision: &rev, + Timestamp: &ts, + } + }(), + validateFunc: func(j JobCreated) { + suite.Equal("11111111-1111-1111-1111-111111111111", j.JobID) + suite.Equal("pending", j.Status) + suite.Equal(int64(42), j.Revision) + suite.Equal("2026-03-04T12:00:00Z", j.Timestamp) + }, + }, + { + name: "when optional fields are nil", + input: &gen.CreateJobResponse{ + JobId: openapi_types.UUID(uuid.MustParse("22222222-2222-2222-2222-222222222222")), + Status: "pending", + }, + validateFunc: func(j JobCreated) { + suite.Equal("22222222-2222-2222-2222-222222222222", j.JobID) + suite.Equal("pending", j.Status) + suite.Equal(int64(0), j.Revision) + suite.Empty(j.Timestamp) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := jobCreatedFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *JobTypesTestSuite) TestJobDetailFromGen() { + tests := []struct { + name string + input *gen.JobDetailResponse + validateFunc func(JobDetail) + }{ + { + name: "when all fields are populated with agent states responses and timeline", + input: func() *gen.JobDetailResponse { + id := openapi_types.UUID(uuid.MustParse("33333333-3333-3333-3333-333333333333")) + status := "completed" + hostname := "web-01" + created := "2026-03-04T12:00:00Z" + updatedAt := "2026-03-04T12:01:00Z" + errMsg := "something failed" + operation := map[string]interface{}{"type": "node.hostname"} + result := map[string]interface{}{"hostname": "web-01"} + + agentStatus := "completed" + agentDuration := "1.5s" + agentError := "" + agentStates := map[string]struct { + Duration *string `json:"duration,omitempty"` + Error *string `json:"error,omitempty"` + Status *string `json:"status,omitempty"` + }{ + "web-01": { + Status: &agentStatus, + Duration: &agentDuration, + Error: &agentError, + }, + } + + respHostname := "web-01" + respStatus := "completed" + respError := "" + respData := map[string]interface{}{"hostname": "web-01"} + responses := map[string]struct { + Data interface{} `json:"data,omitempty"` + Error *string `json:"error,omitempty"` + Hostname *string `json:"hostname,omitempty"` + Status *string `json:"status,omitempty"` + }{ + "web-01": { + Hostname: &respHostname, + Status: &respStatus, + Error: &respError, + Data: respData, + }, + } + + tlTimestamp := "2026-03-04T12:00:00Z" + tlEvent := "submitted" + tlHostname := "api-server" + tlMessage := "Job submitted" + tlError := "" + timeline := []struct { + Error *string `json:"error,omitempty"` + Event *string `json:"event,omitempty"` + Hostname *string `json:"hostname,omitempty"` + Message *string `json:"message,omitempty"` + Timestamp *string `json:"timestamp,omitempty"` + }{ + { + Timestamp: &tlTimestamp, + Event: &tlEvent, + Hostname: &tlHostname, + Message: &tlMessage, + Error: &tlError, + }, + } + + return &gen.JobDetailResponse{ + Id: &id, + Status: &status, + Hostname: &hostname, + Created: &created, + UpdatedAt: &updatedAt, + Error: &errMsg, + Operation: &operation, + Result: result, + AgentStates: &agentStates, + Responses: &responses, + Timeline: &timeline, + } + }(), + validateFunc: func(j JobDetail) { + suite.Equal("33333333-3333-3333-3333-333333333333", j.ID) + suite.Equal("completed", j.Status) + suite.Equal("web-01", j.Hostname) + suite.Equal("2026-03-04T12:00:00Z", j.Created) + suite.Equal("2026-03-04T12:01:00Z", j.UpdatedAt) + suite.Equal("something failed", j.Error) + suite.Equal(map[string]interface{}{"type": "node.hostname"}, j.Operation) + suite.Equal(map[string]interface{}{"hostname": "web-01"}, j.Result) + + suite.Len(j.AgentStates, 1) + suite.Equal("completed", j.AgentStates["web-01"].Status) + suite.Equal("1.5s", j.AgentStates["web-01"].Duration) + suite.Empty(j.AgentStates["web-01"].Error) + + suite.Len(j.Responses, 1) + suite.Equal("web-01", j.Responses["web-01"].Hostname) + suite.Equal("completed", j.Responses["web-01"].Status) + suite.Empty(j.Responses["web-01"].Error) + suite.Equal( + map[string]interface{}{"hostname": "web-01"}, + j.Responses["web-01"].Data, + ) + + suite.Len(j.Timeline, 1) + suite.Equal("2026-03-04T12:00:00Z", j.Timeline[0].Timestamp) + suite.Equal("submitted", j.Timeline[0].Event) + suite.Equal("api-server", j.Timeline[0].Hostname) + suite.Equal("Job submitted", j.Timeline[0].Message) + suite.Empty(j.Timeline[0].Error) + }, + }, + { + name: "when all fields are nil", + input: &gen.JobDetailResponse{}, + validateFunc: func(j JobDetail) { + suite.Empty(j.ID) + suite.Empty(j.Status) + suite.Empty(j.Hostname) + suite.Empty(j.Created) + suite.Empty(j.UpdatedAt) + suite.Empty(j.Error) + suite.Nil(j.Operation) + suite.Nil(j.Result) + suite.Nil(j.AgentStates) + suite.Nil(j.Responses) + suite.Nil(j.Timeline) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := jobDetailFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *JobTypesTestSuite) TestJobListFromGen() { + tests := []struct { + name string + input *gen.ListJobsResponse + validateFunc func(JobList) + }{ + { + name: "when items are present", + input: func() *gen.ListJobsResponse { + id := openapi_types.UUID(uuid.MustParse("44444444-4444-4444-4444-444444444444")) + status := "pending" + totalItems := 1 + items := []gen.JobDetailResponse{ + { + Id: &id, + Status: &status, + }, + } + + return &gen.ListJobsResponse{ + Items: &items, + TotalItems: &totalItems, + } + }(), + validateFunc: func(jl JobList) { + suite.Equal(1, jl.TotalItems) + suite.Len(jl.Items, 1) + suite.Equal("44444444-4444-4444-4444-444444444444", jl.Items[0].ID) + suite.Equal("pending", jl.Items[0].Status) + }, + }, + { + name: "when items are empty", + input: func() *gen.ListJobsResponse { + totalItems := 0 + items := []gen.JobDetailResponse{} + + return &gen.ListJobsResponse{ + Items: &items, + TotalItems: &totalItems, + } + }(), + validateFunc: func(jl JobList) { + suite.Equal(0, jl.TotalItems) + suite.Empty(jl.Items) + }, + }, + { + name: "when all fields are nil", + input: &gen.ListJobsResponse{}, + validateFunc: func(jl JobList) { + suite.Equal(0, jl.TotalItems) + suite.Nil(jl.Items) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := jobListFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *JobTypesTestSuite) TestQueueStatsFromGen() { + tests := []struct { + name string + input *gen.QueueStatsResponse + validateFunc func(QueueStats) + }{ + { + name: "when all fields are populated", + input: func() *gen.QueueStatsResponse { + totalJobs := 100 + dlqCount := 5 + statusCounts := map[string]int{ + "pending": 30, + "completed": 60, + "failed": 10, + } + operationCounts := map[string]int{ + "node.hostname": 50, + "node.disk": 50, + } + + return &gen.QueueStatsResponse{ + TotalJobs: &totalJobs, + DlqCount: &dlqCount, + StatusCounts: &statusCounts, + OperationCounts: &operationCounts, + } + }(), + validateFunc: func(qs QueueStats) { + suite.Equal(100, qs.TotalJobs) + suite.Equal(5, qs.DlqCount) + suite.Equal(map[string]int{ + "pending": 30, + "completed": 60, + "failed": 10, + }, qs.StatusCounts) + suite.Equal(map[string]int{ + "node.hostname": 50, + "node.disk": 50, + }, qs.OperationCounts) + }, + }, + { + name: "when all fields are nil", + input: &gen.QueueStatsResponse{}, + validateFunc: func(qs QueueStats) { + suite.Equal(0, qs.TotalJobs) + suite.Equal(0, qs.DlqCount) + suite.Nil(qs.StatusCounts) + suite.Nil(qs.OperationCounts) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := queueStatsFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func TestJobTypesTestSuite(t *testing.T) { + suite.Run(t, new(JobTypesTestSuite)) +} diff --git a/pkg/osapi/node.go b/pkg/osapi/node.go index 2a7c511..dd4515f 100644 --- a/pkg/osapi/node.go +++ b/pkg/osapi/node.go @@ -22,6 +22,7 @@ package osapi import ( "context" + "fmt" "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" ) @@ -71,56 +72,168 @@ type ShellRequest struct { func (s *NodeService) Status( ctx context.Context, target string, -) (*gen.GetNodeStatusResponse, error) { - return s.client.GetNodeStatusWithResponse(ctx, target) +) (*Response[Collection[NodeStatus]], error) { + resp, err := s.client.GetNodeStatusWithResponse(ctx, target) + if err != nil { + return nil, fmt.Errorf("get status: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(nodeStatusCollectionFromGen(resp.JSON200), resp.Body), nil } // Hostname retrieves the hostname from the target host. func (s *NodeService) Hostname( ctx context.Context, target string, -) (*gen.GetNodeHostnameResponse, error) { - return s.client.GetNodeHostnameWithResponse(ctx, target) +) (*Response[Collection[HostnameResult]], error) { + resp, err := s.client.GetNodeHostnameWithResponse(ctx, target) + if err != nil { + return nil, fmt.Errorf("get hostname: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(hostnameCollectionFromGen(resp.JSON200), resp.Body), nil } // Disk retrieves disk usage information from the target host. func (s *NodeService) Disk( ctx context.Context, target string, -) (*gen.GetNodeDiskResponse, error) { - return s.client.GetNodeDiskWithResponse(ctx, target) +) (*Response[Collection[DiskResult]], error) { + resp, err := s.client.GetNodeDiskWithResponse(ctx, target) + if err != nil { + return nil, fmt.Errorf("get disk: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(diskCollectionFromGen(resp.JSON200), resp.Body), nil } // Memory retrieves memory usage information from the target host. func (s *NodeService) Memory( ctx context.Context, target string, -) (*gen.GetNodeMemoryResponse, error) { - return s.client.GetNodeMemoryWithResponse(ctx, target) +) (*Response[Collection[MemoryResult]], error) { + resp, err := s.client.GetNodeMemoryWithResponse(ctx, target) + if err != nil { + return nil, fmt.Errorf("get memory: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(memoryCollectionFromGen(resp.JSON200), resp.Body), nil } // Load retrieves load average information from the target host. func (s *NodeService) Load( ctx context.Context, target string, -) (*gen.GetNodeLoadResponse, error) { - return s.client.GetNodeLoadWithResponse(ctx, target) +) (*Response[Collection[LoadResult]], error) { + resp, err := s.client.GetNodeLoadWithResponse(ctx, target) + if err != nil { + return nil, fmt.Errorf("get load: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(loadCollectionFromGen(resp.JSON200), resp.Body), nil } // OS retrieves operating system information from the target host. func (s *NodeService) OS( ctx context.Context, target string, -) (*gen.GetNodeOSResponse, error) { - return s.client.GetNodeOSWithResponse(ctx, target) +) (*Response[Collection[OSInfoResult]], error) { + resp, err := s.client.GetNodeOSWithResponse(ctx, target) + if err != nil { + return nil, fmt.Errorf("get os: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(osInfoCollectionFromGen(resp.JSON200), resp.Body), nil } // Uptime retrieves uptime information from the target host. func (s *NodeService) Uptime( ctx context.Context, target string, -) (*gen.GetNodeUptimeResponse, error) { - return s.client.GetNodeUptimeWithResponse(ctx, target) +) (*Response[Collection[UptimeResult]], error) { + resp, err := s.client.GetNodeUptimeWithResponse(ctx, target) + if err != nil { + return nil, fmt.Errorf("get uptime: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(uptimeCollectionFromGen(resp.JSON200), resp.Body), nil } // GetDNS retrieves DNS configuration for a network interface on the @@ -129,8 +242,24 @@ func (s *NodeService) GetDNS( ctx context.Context, target string, interfaceName string, -) (*gen.GetNodeNetworkDNSByInterfaceResponse, error) { - return s.client.GetNodeNetworkDNSByInterfaceWithResponse(ctx, target, interfaceName) +) (*Response[Collection[DNSConfig]], error) { + resp, err := s.client.GetNodeNetworkDNSByInterfaceWithResponse(ctx, target, interfaceName) + if err != nil { + return nil, fmt.Errorf("get dns: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(dnsConfigCollectionFromGen(resp.JSON200), resp.Body), nil } // UpdateDNS updates DNS configuration for a network interface on the @@ -141,7 +270,7 @@ func (s *NodeService) UpdateDNS( interfaceName string, servers []string, searchDomains []string, -) (*gen.PutNodeNetworkDNSResponse, error) { +) (*Response[Collection[DNSUpdateResult]], error) { body := gen.DNSConfigUpdateRequest{ InterfaceName: interfaceName, } @@ -154,7 +283,23 @@ func (s *NodeService) UpdateDNS( body.SearchDomains = &searchDomains } - return s.client.PutNodeNetworkDNSWithResponse(ctx, target, body) + resp, err := s.client.PutNodeNetworkDNSWithResponse(ctx, target, body) + if err != nil { + return nil, fmt.Errorf("update dns: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON202 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(dnsUpdateCollectionFromGen(resp.JSON202), resp.Body), nil } // Ping sends an ICMP ping to the specified address from the target host. @@ -162,19 +307,35 @@ func (s *NodeService) Ping( ctx context.Context, target string, address string, -) (*gen.PostNodeNetworkPingResponse, error) { +) (*Response[Collection[PingResult]], error) { body := gen.PostNodeNetworkPingJSONRequestBody{ Address: address, } - return s.client.PostNodeNetworkPingWithResponse(ctx, target, body) + resp, err := s.client.PostNodeNetworkPingWithResponse(ctx, target, body) + if err != nil { + return nil, fmt.Errorf("ping: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON200 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(pingCollectionFromGen(resp.JSON200), resp.Body), nil } // Exec executes a command directly without a shell interpreter. func (s *NodeService) Exec( ctx context.Context, req ExecRequest, -) (*gen.PostNodeCommandExecResponse, error) { +) (*Response[Collection[CommandResult]], error) { body := gen.CommandExecRequest{ Command: req.Command, } @@ -191,7 +352,23 @@ func (s *NodeService) Exec( body.Timeout = &req.Timeout } - return s.client.PostNodeCommandExecWithResponse(ctx, req.Target, body) + resp, err := s.client.PostNodeCommandExecWithResponse(ctx, req.Target, body) + if err != nil { + return nil, fmt.Errorf("exec command: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON202 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(commandCollectionFromGen(resp.JSON202), resp.Body), nil } // Shell executes a command through /bin/sh -c with shell features @@ -199,7 +376,7 @@ func (s *NodeService) Exec( func (s *NodeService) Shell( ctx context.Context, req ShellRequest, -) (*gen.PostNodeCommandShellResponse, error) { +) (*Response[Collection[CommandResult]], error) { body := gen.CommandShellRequest{ Command: req.Command, } @@ -212,5 +389,21 @@ func (s *NodeService) Shell( body.Timeout = &req.Timeout } - return s.client.PostNodeCommandShellWithResponse(ctx, req.Target, body) + resp, err := s.client.PostNodeCommandShellWithResponse(ctx, req.Target, body) + if err != nil { + return nil, fmt.Errorf("shell command: %w", err) + } + + if err := checkError(resp.StatusCode(), resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON500); err != nil { + return nil, err + } + + if resp.JSON202 == nil { + return nil, &UnexpectedStatusError{APIError{ + StatusCode: resp.StatusCode(), + Message: "nil response body", + }} + } + + return NewResponse(commandCollectionFromGen(resp.JSON202), resp.Body), nil } diff --git a/pkg/osapi/node_public_test.go b/pkg/osapi/node_public_test.go index 568632c..e4752d9 100644 --- a/pkg/osapi/node_public_test.go +++ b/pkg/osapi/node_public_test.go @@ -22,6 +22,7 @@ package osapi_test import ( "context" + "errors" "log/slog" "net/http" "net/http/httptest" @@ -35,382 +36,2011 @@ import ( type NodePublicTestSuite struct { suite.Suite - ctx context.Context - server *httptest.Server - sut *osapi.Client + ctx context.Context } func (suite *NodePublicTestSuite) SetupTest() { suite.ctx = context.Background() - - suite.server = httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{}`)) - }), - ) - - suite.sut = osapi.New( - suite.server.URL, - "test-token", - osapi.WithLogger(slog.Default()), - ) -} - -func (suite *NodePublicTestSuite) TearDownTest() { - suite.server.Close() } func (suite *NodePublicTestSuite) TestHostname() { tests := []struct { name string target string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.HostnameResult]], error) }{ { - name: "when requesting hostname returns no error", + name: "when requesting hostname returns results", target: "_any", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.HostnameResult]], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Equal("00000000-0000-0000-0000-000000000001", resp.Data.JobID) + suite.Len(resp.Data.Results, 1) + suite.Equal("test-host", resp.Data.Results[0].Hostname) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.Hostname(suite.ctx, tc.target) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"job_id":"00000000-0000-0000-0000-000000000001","results":[{"hostname":"test-host"}]}`, + ), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Hostname(suite.ctx, tc.target) + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestStatus() { +func (suite *NodePublicTestSuite) TestHostnameClientError() { tests := []struct { name string - target string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.HostnameResult]], error) }{ { - name: "when requesting status returns no error", - target: "_any", - validateFunc: func(err error) { - suite.NoError(err) + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.HostnameResult]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get hostname") }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.Status(suite.ctx, tc.target) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Hostname(suite.ctx, "_any") + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestDisk() { +func (suite *NodePublicTestSuite) TestHostnameNilResponse() { tests := []struct { name string - target string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.HostnameResult]], error) }{ { - name: "when requesting disk returns no error", - target: "_any", - validateFunc: func(err error) { - suite.NoError(err) + name: "when server returns 200 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.HostnameResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Equal("nil response body", target.Message) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.Disk(suite.ctx, tc.target) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Hostname(suite.ctx, "_any") + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestMemory() { +func (suite *NodePublicTestSuite) TestHostnameError() { tests := []struct { name string - target string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.HostnameResult]], error) }{ { - name: "when requesting memory returns no error", - target: "_any", - validateFunc: func(err error) { - suite.NoError(err) + name: "when server returns 403 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.HostnameResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.Memory(suite.ctx, tc.target) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Hostname(suite.ctx, "_any") + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestLoad() { +func (suite *NodePublicTestSuite) TestStatus() { tests := []struct { name string target string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.NodeStatus]], error) }{ { - name: "when requesting load returns no error", + name: "when requesting status returns results", target: "_any", - validateFunc: func(err error) { + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.NodeStatus]], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("web-01", resp.Data.Results[0].Hostname) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.Load(suite.ctx, tc.target) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"results":[{"hostname":"web-01"}]}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Status(suite.ctx, tc.target) + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestOS() { +func (suite *NodePublicTestSuite) TestStatusError() { tests := []struct { name string - target string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.NodeStatus]], error) }{ { - name: "when requesting OS info returns no error", - target: "_any", - validateFunc: func(err error) { - suite.NoError(err) + name: "when server returns 403 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.NodeStatus]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.OS(suite.ctx, tc.target) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Status(suite.ctx, "_any") + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestUptime() { +func (suite *NodePublicTestSuite) TestStatusClientError() { tests := []struct { name string - target string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.NodeStatus]], error) }{ { - name: "when requesting uptime returns no error", - target: "_any", - validateFunc: func(err error) { - suite.NoError(err) + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.NodeStatus]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get status") }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.Uptime(suite.ctx, tc.target) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Status(suite.ctx, "_any") + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestGetDNS() { +func (suite *NodePublicTestSuite) TestStatusNilResponse() { tests := []struct { name string - target string - iface string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.NodeStatus]], error) }{ { - name: "when requesting DNS returns no error", - target: "_any", - iface: "eth0", - validateFunc: func(err error) { - suite.NoError(err) + name: "when server returns 200 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.NodeStatus]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Equal("nil response body", target.Message) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.GetDNS(suite.ctx, tc.target, tc.iface) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Status(suite.ctx, "_any") + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestUpdateDNS() { +func (suite *NodePublicTestSuite) TestDisk() { tests := []struct { - name string - target string - iface string - servers []string - searchDomains []string - validateFunc func(error) + name string + target string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DiskResult]], error) }{ { - name: "when servers only provided sets servers", - target: "_any", - iface: "eth0", - servers: []string{"8.8.8.8", "8.8.4.4"}, - searchDomains: nil, - validateFunc: func(err error) { + name: "when requesting disk returns results", + target: "_any", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DiskResult]], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("disk-host", resp.Data.Results[0].Hostname) }, }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"results":[{"hostname":"disk-host"}]}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Disk(suite.ctx, tc.target) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestDiskError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DiskResult]], error) + }{ { - name: "when search domains only provided sets search domains", - target: "_any", - iface: "eth0", - servers: nil, - searchDomains: []string{"example.com"}, - validateFunc: func(err error) { - suite.NoError(err) + name: "when server returns 403 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DiskResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) }, }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Disk(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestDiskClientError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DiskResult]], error) + }{ { - name: "when both provided sets servers and search domains", - target: "_any", - iface: "eth0", - servers: []string{"8.8.8.8"}, - searchDomains: []string{"example.com"}, - validateFunc: func(err error) { - suite.NoError(err) + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DiskResult]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get disk") }, }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Disk(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestDiskNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DiskResult]], error) + }{ { - name: "when neither provided sends empty body", - target: "_any", - iface: "eth0", - servers: nil, - searchDomains: nil, - validateFunc: func(err error) { - suite.NoError(err) + name: "when server returns 200 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DiskResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Equal("nil response body", target.Message) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.UpdateDNS( - suite.ctx, - tc.target, - tc.iface, - tc.servers, - tc.searchDomains, + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), ) - tc.validateFunc(err) + + resp, err := sut.Node.Disk(suite.ctx, "_any") + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestPing() { +func (suite *NodePublicTestSuite) TestMemory() { tests := []struct { name string target string - address string - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.MemoryResult]], error) }{ { - name: "when pinging address returns no error", - target: "_any", - address: "8.8.8.8", - validateFunc: func(err error) { + name: "when requesting memory returns results", + target: "_any", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.MemoryResult]], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("mem-host", resp.Data.Results[0].Hostname) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.Ping(suite.ctx, tc.target, tc.address) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"results":[{"hostname":"mem-host"}]}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Memory(suite.ctx, tc.target) + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestExec() { +func (suite *NodePublicTestSuite) TestMemoryError() { tests := []struct { name string - req osapi.ExecRequest - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.MemoryResult]], error) }{ { - name: "when basic command returns no error", - req: osapi.ExecRequest{ - Command: "whoami", - Target: "_any", - }, - validateFunc: func(err error) { - suite.NoError(err) + name: "when server returns 403 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.MemoryResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) }, }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Memory(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestMemoryClientError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.MemoryResult]], error) + }{ { - name: "when all options provided returns no error", - req: osapi.ExecRequest{ - Command: "ls", - Args: []string{"-la", "/tmp"}, - Cwd: "/tmp", - Timeout: 10, - Target: "_any", - }, - validateFunc: func(err error) { - suite.NoError(err) + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.MemoryResult]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get memory") }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.Exec(suite.ctx, tc.req) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Memory(suite.ctx, "_any") + tc.validateFunc(resp, err) }) } } -func (suite *NodePublicTestSuite) TestShell() { +func (suite *NodePublicTestSuite) TestMemoryNilResponse() { tests := []struct { name string - req osapi.ShellRequest - validateFunc func(error) + validateFunc func(*osapi.Response[osapi.Collection[osapi.MemoryResult]], error) }{ { - name: "when basic command returns no error", - req: osapi.ShellRequest{ - Command: "uname -a", - Target: "_any", + name: "when server returns 200 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.MemoryResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Equal("nil response body", target.Message) }, - validateFunc: func(err error) { + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Memory(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestLoad() { + tests := []struct { + name string + target string + validateFunc func(*osapi.Response[osapi.Collection[osapi.LoadResult]], error) + }{ + { + name: "when requesting load returns results", + target: "_any", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.LoadResult]], err error) { suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("load-host", resp.Data.Results[0].Hostname) }, }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"results":[{"hostname":"load-host"}]}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Load(suite.ctx, tc.target) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestLoadError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.LoadResult]], error) + }{ { - name: "when cwd and timeout provided returns no error", - req: osapi.ShellRequest{ - Command: "ls -la", - Cwd: "/var/log", - Timeout: 15, - Target: "_any", - }, - validateFunc: func(err error) { - suite.NoError(err) + name: "when server returns 403 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.LoadResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) }, }, } for _, tc := range tests { suite.Run(tc.name, func() { - _, err := suite.sut.Node.Shell(suite.ctx, tc.req) - tc.validateFunc(err) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Load(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestLoadClientError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.LoadResult]], error) + }{ + { + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.LoadResult]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get load") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Load(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestLoadNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.LoadResult]], error) + }{ + { + name: "when server returns 200 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.LoadResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Equal("nil response body", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Load(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestOS() { + tests := []struct { + name string + target string + validateFunc func(*osapi.Response[osapi.Collection[osapi.OSInfoResult]], error) + }{ + { + name: "when requesting OS info returns results", + target: "_any", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.OSInfoResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("os-host", resp.Data.Results[0].Hostname) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"results":[{"hostname":"os-host"}]}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.OS(suite.ctx, tc.target) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestOSError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.OSInfoResult]], error) + }{ + { + name: "when server returns 403 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.OSInfoResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.OS(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestOSClientError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.OSInfoResult]], error) + }{ + { + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.OSInfoResult]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get os") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.OS(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestOSNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.OSInfoResult]], error) + }{ + { + name: "when server returns 200 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.OSInfoResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Equal("nil response body", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.OS(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestUptime() { + tests := []struct { + name string + target string + validateFunc func(*osapi.Response[osapi.Collection[osapi.UptimeResult]], error) + }{ + { + name: "when requesting uptime returns results", + target: "_any", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.UptimeResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("uptime-host", resp.Data.Results[0].Hostname) + suite.Equal("2d3h", resp.Data.Results[0].Uptime) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte(`{"results":[{"hostname":"uptime-host","uptime":"2d3h"}]}`), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Uptime(suite.ctx, tc.target) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestUptimeError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.UptimeResult]], error) + }{ + { + name: "when server returns 403 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.UptimeResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Uptime(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestUptimeClientError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.UptimeResult]], error) + }{ + { + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.UptimeResult]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get uptime") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Uptime(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestUptimeNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.UptimeResult]], error) + }{ + { + name: "when server returns 200 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.UptimeResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Equal("nil response body", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Uptime(suite.ctx, "_any") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestGetDNS() { + tests := []struct { + name string + target string + iface string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DNSConfig]], error) + }{ + { + name: "when requesting DNS returns results", + target: "_any", + iface: "eth0", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSConfig]], err error) { + suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("dns-host", resp.Data.Results[0].Hostname) + suite.Equal([]string{"8.8.8.8"}, resp.Data.Results[0].Servers) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte(`{"results":[{"hostname":"dns-host","servers":["8.8.8.8"]}]}`), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.GetDNS(suite.ctx, tc.target, tc.iface) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestGetDNSError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DNSConfig]], error) + }{ + { + name: "when server returns 403 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSConfig]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.GetDNS(suite.ctx, "_any", "eth0") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestGetDNSClientError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DNSConfig]], error) + }{ + { + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSConfig]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "get dns") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.GetDNS(suite.ctx, "_any", "eth0") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestGetDNSNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DNSConfig]], error) + }{ + { + name: "when server returns 200 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSConfig]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Equal("nil response body", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.GetDNS(suite.ctx, "_any", "eth0") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestUpdateDNS() { + tests := []struct { + name string + target string + iface string + servers []string + searchDomains []string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], error) + }{ + { + name: "when servers only provided sets servers", + target: "_any", + iface: "eth0", + servers: []string{"8.8.8.8", "8.8.4.4"}, + searchDomains: nil, + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("dns-host", resp.Data.Results[0].Hostname) + suite.Equal("completed", resp.Data.Results[0].Status) + suite.True(resp.Data.Results[0].Changed) + }, + }, + { + name: "when search domains only provided sets search domains", + target: "_any", + iface: "eth0", + servers: nil, + searchDomains: []string{"example.com"}, + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + }, + }, + { + name: "when both provided sets servers and search domains", + target: "_any", + iface: "eth0", + servers: []string{"8.8.8.8"}, + searchDomains: []string{"example.com"}, + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + }, + }, + { + name: "when neither provided sends empty body", + target: "_any", + iface: "eth0", + servers: nil, + searchDomains: nil, + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"dns-host","status":"completed","changed":true}]}`, + ), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.UpdateDNS( + suite.ctx, + tc.target, + tc.iface, + tc.servers, + tc.searchDomains, + ) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestUpdateDNSError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], error) + }{ + { + name: "when server returns 403 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.UpdateDNS(suite.ctx, "_any", "eth0", []string{"8.8.8.8"}, nil) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestUpdateDNSClientError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], error) + }{ + { + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "update dns") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.UpdateDNS(suite.ctx, "_any", "eth0", []string{"8.8.8.8"}, nil) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestUpdateDNSNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], error) + }{ + { + name: "when server returns 202 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusAccepted, target.StatusCode) + suite.Equal("nil response body", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusAccepted) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.UpdateDNS(suite.ctx, "_any", "eth0", []string{"8.8.8.8"}, nil) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestPing() { + tests := []struct { + name string + target string + address string + validateFunc func(*osapi.Response[osapi.Collection[osapi.PingResult]], error) + }{ + { + name: "when pinging address returns results", + target: "_any", + address: "8.8.8.8", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.PingResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("ping-host", resp.Data.Results[0].Hostname) + suite.Equal(4, resp.Data.Results[0].PacketsSent) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"ping-host","packets_sent":4,"packets_received":4,"packet_loss":0.0}]}`, + ), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Ping(suite.ctx, tc.target, tc.address) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestPingError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.PingResult]], error) + }{ + { + name: "when server returns 403 returns AuthError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.PingResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.AuthError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusForbidden, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"forbidden"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Ping(suite.ctx, "_any", "8.8.8.8") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestPingClientError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.PingResult]], error) + }{ + { + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.PingResult]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "ping") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Ping(suite.ctx, "_any", "8.8.8.8") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestPingNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.PingResult]], error) + }{ + { + name: "when server returns 200 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.PingResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusOK, target.StatusCode) + suite.Equal("nil response body", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Ping(suite.ctx, "_any", "8.8.8.8") + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestExec() { + tests := []struct { + name string + req osapi.ExecRequest + validateFunc func(*osapi.Response[osapi.Collection[osapi.CommandResult]], error) + }{ + { + name: "when basic command returns results", + req: osapi.ExecRequest{ + Command: "whoami", + Target: "_any", + }, + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("exec-host", resp.Data.Results[0].Hostname) + suite.Equal("root\n", resp.Data.Results[0].Stdout) + suite.Equal(0, resp.Data.Results[0].ExitCode) + }, + }, + { + name: "when all options provided returns results", + req: osapi.ExecRequest{ + Command: "ls", + Args: []string{"-la", "/tmp"}, + Cwd: "/tmp", + Timeout: 10, + Target: "_any", + }, + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"exec-host","stdout":"root\n","exit_code":0,"changed":true}]}`, + ), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Exec(suite.ctx, tc.req) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestExecClientError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.CommandResult]], error) + }{ + { + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "exec command") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Exec(suite.ctx, osapi.ExecRequest{ + Command: "whoami", + Target: "_any", + }) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestExecNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.CommandResult]], error) + }{ + { + name: "when server returns 202 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusAccepted, target.StatusCode) + suite.Equal("nil response body", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusAccepted) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Exec(suite.ctx, osapi.ExecRequest{ + Command: "whoami", + Target: "_any", + }) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestExecError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.CommandResult]], error) + }{ + { + name: "when server returns 400 returns ValidationError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.ValidationError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusBadRequest, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"error":"command is required"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Exec(suite.ctx, osapi.ExecRequest{ + Target: "_any", + }) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestShell() { + tests := []struct { + name string + req osapi.ShellRequest + validateFunc func(*osapi.Response[osapi.Collection[osapi.CommandResult]], error) + }{ + { + name: "when basic command returns results", + req: osapi.ShellRequest{ + Command: "uname -a", + Target: "_any", + }, + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + suite.Len(resp.Data.Results, 1) + suite.Equal("shell-host", resp.Data.Results[0].Hostname) + }, + }, + { + name: "when cwd and timeout provided returns results", + req: osapi.ShellRequest{ + Command: "ls -la", + Cwd: "/var/log", + Timeout: 15, + Target: "_any", + }, + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) { + suite.NoError(err) + suite.NotNil(resp) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"shell-host","exit_code":0,"changed":false}]}`, + ), + ) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Shell(suite.ctx, tc.req) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestShellClientError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.CommandResult]], error) + }{ + { + name: "when client HTTP call fails returns error", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) { + suite.Error(err) + suite.Nil(resp) + suite.Contains(err.Error(), "shell command") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}), + ) + server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Shell(suite.ctx, osapi.ShellRequest{ + Command: "uname -a", + Target: "_any", + }) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestShellNilResponse() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.CommandResult]], error) + }{ + { + name: "when server returns 202 with no JSON body returns UnexpectedStatusError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusAccepted, target.StatusCode) + suite.Equal("nil response body", target.Message) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusAccepted) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Shell(suite.ctx, osapi.ShellRequest{ + Command: "uname -a", + Target: "_any", + }) + tc.validateFunc(resp, err) + }) + } +} + +func (suite *NodePublicTestSuite) TestShellError() { + tests := []struct { + name string + validateFunc func(*osapi.Response[osapi.Collection[osapi.CommandResult]], error) + }{ + { + name: "when server returns 400 returns ValidationError", + validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) { + suite.Error(err) + suite.Nil(resp) + + var target *osapi.ValidationError + suite.True(errors.As(err, &target)) + suite.Equal(http.StatusBadRequest, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"error":"command is required"}`)) + }), + ) + defer server.Close() + + sut := osapi.New( + server.URL, + "test-token", + osapi.WithLogger(slog.Default()), + ) + + resp, err := sut.Node.Shell(suite.ctx, osapi.ShellRequest{ + Target: "_any", + }) + tc.validateFunc(resp, err) }) } } diff --git a/pkg/osapi/node_types.go b/pkg/osapi/node_types.go new file mode 100644 index 0000000..e85595f --- /dev/null +++ b/pkg/osapi/node_types.go @@ -0,0 +1,501 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + openapi_types "github.com/oapi-codegen/runtime/types" + + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +// Collection is a generic wrapper for collection responses from node queries. +type Collection[T any] struct { + Results []T + JobID string +} + +// Disk represents disk usage information. +type Disk struct { + Name string + Total int + Used int + Free int +} + +// HostnameResult represents a hostname query result from a single agent. +type HostnameResult struct { + Hostname string + Error string + Labels map[string]string +} + +// NodeStatus represents full node status from a single agent. +type NodeStatus struct { + Hostname string + Uptime string + Error string + Disks []Disk + LoadAverage *LoadAverage + Memory *Memory + OSInfo *OSInfo +} + +// DiskResult represents disk query result from a single agent. +type DiskResult struct { + Hostname string + Error string + Disks []Disk +} + +// MemoryResult represents memory query result from a single agent. +type MemoryResult struct { + Hostname string + Error string + Memory *Memory +} + +// LoadResult represents load average query result from a single agent. +type LoadResult struct { + Hostname string + Error string + LoadAverage *LoadAverage +} + +// OSInfoResult represents OS info query result from a single agent. +type OSInfoResult struct { + Hostname string + Error string + OSInfo *OSInfo +} + +// UptimeResult represents uptime query result from a single agent. +type UptimeResult struct { + Hostname string + Uptime string + Error string +} + +// DNSConfig represents DNS configuration from a single agent. +type DNSConfig struct { + Hostname string + Error string + Servers []string + SearchDomains []string +} + +// DNSUpdateResult represents DNS update result from a single agent. +type DNSUpdateResult struct { + Hostname string + Status string + Error string + Changed bool +} + +// PingResult represents ping result from a single agent. +type PingResult struct { + Hostname string + Error string + PacketsSent int + PacketsReceived int + PacketLoss float64 + MinRtt string + AvgRtt string + MaxRtt string +} + +// CommandResult represents command execution result from a single agent. +type CommandResult struct { + Hostname string + Stdout string + Stderr string + Error string + ExitCode int + Changed bool + DurationMs int64 +} + +// loadAverageFromGen converts a gen.LoadAverageResponse to a LoadAverage. +func loadAverageFromGen( + g *gen.LoadAverageResponse, +) *LoadAverage { + if g == nil { + return nil + } + + return &LoadAverage{ + OneMin: g.N1min, + FiveMin: g.N5min, + FifteenMin: g.N15min, + } +} + +// memoryFromGen converts a gen.MemoryResponse to a Memory. +func memoryFromGen( + g *gen.MemoryResponse, +) *Memory { + if g == nil { + return nil + } + + return &Memory{ + Total: g.Total, + Used: g.Used, + Free: g.Free, + } +} + +// osInfoFromGen converts a gen.OSInfoResponse to an OSInfo. +func osInfoFromGen( + g *gen.OSInfoResponse, +) *OSInfo { + if g == nil { + return nil + } + + return &OSInfo{ + Distribution: g.Distribution, + Version: g.Version, + } +} + +// disksFromGen converts a gen.DisksResponse to a slice of Disk. +func disksFromGen( + g *gen.DisksResponse, +) []Disk { + if g == nil { + return nil + } + + disks := make([]Disk, 0, len(*g)) + for _, d := range *g { + disks = append(disks, Disk{ + Name: d.Name, + Total: d.Total, + Used: d.Used, + Free: d.Free, + }) + } + + return disks +} + +// derefString safely dereferences a string pointer, returning empty string for nil. +func derefString( + s *string, +) string { + if s == nil { + return "" + } + + return *s +} + +// derefInt safely dereferences an int pointer, returning zero for nil. +func derefInt( + i *int, +) int { + if i == nil { + return 0 + } + + return *i +} + +// derefInt64 safely dereferences an int64 pointer, returning zero for nil. +func derefInt64( + i *int64, +) int64 { + if i == nil { + return 0 + } + + return *i +} + +// derefFloat64 safely dereferences a float64 pointer, returning zero for nil. +func derefFloat64( + f *float64, +) float64 { + if f == nil { + return 0 + } + + return *f +} + +// derefBool safely dereferences a bool pointer, returning false for nil. +func derefBool( + b *bool, +) bool { + if b == nil { + return false + } + + return *b +} + +// jobIDFromGen extracts a job ID string from an optional UUID pointer. +func jobIDFromGen( + id *openapi_types.UUID, +) string { + if id == nil { + return "" + } + + return id.String() +} + +// hostnameCollectionFromGen converts a gen.HostnameCollectionResponse to a Collection[HostnameResult]. +func hostnameCollectionFromGen( + g *gen.HostnameCollectionResponse, +) Collection[HostnameResult] { + results := make([]HostnameResult, 0, len(g.Results)) + for _, r := range g.Results { + hr := HostnameResult{ + Hostname: r.Hostname, + Error: derefString(r.Error), + } + + if r.Labels != nil { + hr.Labels = *r.Labels + } + + results = append(results, hr) + } + + return Collection[HostnameResult]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} + +// nodeStatusCollectionFromGen converts a gen.NodeStatusCollectionResponse to a Collection[NodeStatus]. +func nodeStatusCollectionFromGen( + g *gen.NodeStatusCollectionResponse, +) Collection[NodeStatus] { + results := make([]NodeStatus, 0, len(g.Results)) + for _, r := range g.Results { + results = append(results, NodeStatus{ + Hostname: r.Hostname, + Uptime: derefString(r.Uptime), + Error: derefString(r.Error), + Disks: disksFromGen(r.Disks), + LoadAverage: loadAverageFromGen(r.LoadAverage), + Memory: memoryFromGen(r.Memory), + OSInfo: osInfoFromGen(r.OsInfo), + }) + } + + return Collection[NodeStatus]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} + +// diskCollectionFromGen converts a gen.DiskCollectionResponse to a Collection[DiskResult]. +func diskCollectionFromGen( + g *gen.DiskCollectionResponse, +) Collection[DiskResult] { + results := make([]DiskResult, 0, len(g.Results)) + for _, r := range g.Results { + results = append(results, DiskResult{ + Hostname: r.Hostname, + Error: derefString(r.Error), + Disks: disksFromGen(r.Disks), + }) + } + + return Collection[DiskResult]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} + +// memoryCollectionFromGen converts a gen.MemoryCollectionResponse to a Collection[MemoryResult]. +func memoryCollectionFromGen( + g *gen.MemoryCollectionResponse, +) Collection[MemoryResult] { + results := make([]MemoryResult, 0, len(g.Results)) + for _, r := range g.Results { + results = append(results, MemoryResult{ + Hostname: r.Hostname, + Error: derefString(r.Error), + Memory: memoryFromGen(r.Memory), + }) + } + + return Collection[MemoryResult]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} + +// loadCollectionFromGen converts a gen.LoadCollectionResponse to a Collection[LoadResult]. +func loadCollectionFromGen( + g *gen.LoadCollectionResponse, +) Collection[LoadResult] { + results := make([]LoadResult, 0, len(g.Results)) + for _, r := range g.Results { + results = append(results, LoadResult{ + Hostname: r.Hostname, + Error: derefString(r.Error), + LoadAverage: loadAverageFromGen(r.LoadAverage), + }) + } + + return Collection[LoadResult]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} + +// osInfoCollectionFromGen converts a gen.OSInfoCollectionResponse to a Collection[OSInfoResult]. +func osInfoCollectionFromGen( + g *gen.OSInfoCollectionResponse, +) Collection[OSInfoResult] { + results := make([]OSInfoResult, 0, len(g.Results)) + for _, r := range g.Results { + results = append(results, OSInfoResult{ + Hostname: r.Hostname, + Error: derefString(r.Error), + OSInfo: osInfoFromGen(r.OsInfo), + }) + } + + return Collection[OSInfoResult]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} + +// uptimeCollectionFromGen converts a gen.UptimeCollectionResponse to a Collection[UptimeResult]. +func uptimeCollectionFromGen( + g *gen.UptimeCollectionResponse, +) Collection[UptimeResult] { + results := make([]UptimeResult, 0, len(g.Results)) + for _, r := range g.Results { + results = append(results, UptimeResult{ + Hostname: r.Hostname, + Uptime: derefString(r.Uptime), + Error: derefString(r.Error), + }) + } + + return Collection[UptimeResult]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} + +// dnsConfigCollectionFromGen converts a gen.DNSConfigCollectionResponse to a Collection[DNSConfig]. +func dnsConfigCollectionFromGen( + g *gen.DNSConfigCollectionResponse, +) Collection[DNSConfig] { + results := make([]DNSConfig, 0, len(g.Results)) + for _, r := range g.Results { + dc := DNSConfig{ + Hostname: r.Hostname, + Error: derefString(r.Error), + } + + if r.Servers != nil { + dc.Servers = *r.Servers + } + + if r.SearchDomains != nil { + dc.SearchDomains = *r.SearchDomains + } + + results = append(results, dc) + } + + return Collection[DNSConfig]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} + +// dnsUpdateCollectionFromGen converts a gen.DNSUpdateCollectionResponse to a Collection[DNSUpdateResult]. +func dnsUpdateCollectionFromGen( + g *gen.DNSUpdateCollectionResponse, +) Collection[DNSUpdateResult] { + results := make([]DNSUpdateResult, 0, len(g.Results)) + for _, r := range g.Results { + results = append(results, DNSUpdateResult{ + Hostname: r.Hostname, + Status: string(r.Status), + Error: derefString(r.Error), + Changed: derefBool(r.Changed), + }) + } + + return Collection[DNSUpdateResult]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} + +// pingCollectionFromGen converts a gen.PingCollectionResponse to a Collection[PingResult]. +func pingCollectionFromGen( + g *gen.PingCollectionResponse, +) Collection[PingResult] { + results := make([]PingResult, 0, len(g.Results)) + for _, r := range g.Results { + results = append(results, PingResult{ + Hostname: r.Hostname, + Error: derefString(r.Error), + PacketsSent: derefInt(r.PacketsSent), + PacketsReceived: derefInt(r.PacketsReceived), + PacketLoss: derefFloat64(r.PacketLoss), + MinRtt: derefString(r.MinRtt), + AvgRtt: derefString(r.AvgRtt), + MaxRtt: derefString(r.MaxRtt), + }) + } + + return Collection[PingResult]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} + +// commandCollectionFromGen converts a gen.CommandResultCollectionResponse to a Collection[CommandResult]. +func commandCollectionFromGen( + g *gen.CommandResultCollectionResponse, +) Collection[CommandResult] { + results := make([]CommandResult, 0, len(g.Results)) + for _, r := range g.Results { + results = append(results, CommandResult{ + Hostname: r.Hostname, + Stdout: derefString(r.Stdout), + Stderr: derefString(r.Stderr), + Error: derefString(r.Error), + ExitCode: derefInt(r.ExitCode), + Changed: derefBool(r.Changed), + DurationMs: derefInt64(r.DurationMs), + }) + } + + return Collection[CommandResult]{ + Results: results, + JobID: jobIDFromGen(g.JobId), + } +} diff --git a/pkg/osapi/node_types_test.go b/pkg/osapi/node_types_test.go new file mode 100644 index 0000000..577df8c --- /dev/null +++ b/pkg/osapi/node_types_test.go @@ -0,0 +1,838 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + "testing" + + openapi_types "github.com/oapi-codegen/runtime/types" + "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +type NodeTypesTestSuite struct { + suite.Suite +} + +func (suite *NodeTypesTestSuite) TestLoadAverageFromGen() { + tests := []struct { + name string + input *gen.LoadAverageResponse + validateFunc func(*LoadAverage) + }{ + { + name: "when populated", + input: &gen.LoadAverageResponse{ + N1min: 0.5, + N5min: 1.2, + N15min: 0.8, + }, + validateFunc: func(la *LoadAverage) { + suite.Require().NotNil(la) + suite.InDelta(0.5, float64(la.OneMin), 0.001) + suite.InDelta(1.2, float64(la.FiveMin), 0.001) + suite.InDelta(0.8, float64(la.FifteenMin), 0.001) + }, + }, + { + name: "when nil", + input: nil, + validateFunc: func(la *LoadAverage) { + suite.Nil(la) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := loadAverageFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestMemoryFromGen() { + tests := []struct { + name string + input *gen.MemoryResponse + validateFunc func(*Memory) + }{ + { + name: "when populated", + input: &gen.MemoryResponse{ + Total: 8589934592, + Used: 4294967296, + Free: 4294967296, + }, + validateFunc: func(m *Memory) { + suite.Require().NotNil(m) + suite.Equal(8589934592, m.Total) + suite.Equal(4294967296, m.Used) + suite.Equal(4294967296, m.Free) + }, + }, + { + name: "when nil", + input: nil, + validateFunc: func(m *Memory) { + suite.Nil(m) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := memoryFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestOSInfoFromGen() { + tests := []struct { + name string + input *gen.OSInfoResponse + validateFunc func(*OSInfo) + }{ + { + name: "when populated", + input: &gen.OSInfoResponse{ + Distribution: "Ubuntu", + Version: "22.04", + }, + validateFunc: func(oi *OSInfo) { + suite.Require().NotNil(oi) + suite.Equal("Ubuntu", oi.Distribution) + suite.Equal("22.04", oi.Version) + }, + }, + { + name: "when nil", + input: nil, + validateFunc: func(oi *OSInfo) { + suite.Nil(oi) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := osInfoFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestDisksFromGen() { + tests := []struct { + name string + input *gen.DisksResponse + validateFunc func([]Disk) + }{ + { + name: "when populated", + input: func() *gen.DisksResponse { + d := gen.DisksResponse{ + { + Name: "/dev/sda1", + Total: 500000000000, + Used: 250000000000, + Free: 250000000000, + }, + { + Name: "/dev/sdb1", + Total: 1000000000000, + Used: 100000000000, + Free: 900000000000, + }, + } + + return &d + }(), + validateFunc: func(disks []Disk) { + suite.Require().Len(disks, 2) + suite.Equal("/dev/sda1", disks[0].Name) + suite.Equal(500000000000, disks[0].Total) + suite.Equal(250000000000, disks[0].Used) + suite.Equal(250000000000, disks[0].Free) + suite.Equal("/dev/sdb1", disks[1].Name) + }, + }, + { + name: "when nil", + input: nil, + validateFunc: func(disks []Disk) { + suite.Nil(disks) + }, + }, + { + name: "when empty", + input: func() *gen.DisksResponse { + d := gen.DisksResponse{} + + return &d + }(), + validateFunc: func(disks []Disk) { + suite.NotNil(disks) + suite.Empty(disks) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := disksFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestHostnameCollectionFromGen() { + testUUID := openapi_types.UUID{ + 0x55, + 0x0e, + 0x84, + 0x00, + 0xe2, + 0x9b, + 0x41, + 0xd4, + 0xa7, + 0x16, + 0x44, + 0x66, + 0x55, + 0x44, + 0x00, + 0x00, + } + + tests := []struct { + name string + input *gen.HostnameCollectionResponse + validateFunc func(Collection[HostnameResult]) + }{ + { + name: "when all fields are populated", + input: func() *gen.HostnameCollectionResponse { + labels := map[string]string{"group": "web", "env": "prod"} + errMsg := "timeout" + + return &gen.HostnameCollectionResponse{ + JobId: &testUUID, + Results: []gen.HostnameResponse{ + { + Hostname: "web-01", + Labels: &labels, + }, + { + Hostname: "web-02", + Error: &errMsg, + }, + }, + } + }(), + validateFunc: func(c Collection[HostnameResult]) { + suite.Equal("550e8400-e29b-41d4-a716-446655440000", c.JobID) + suite.Require().Len(c.Results, 2) + + suite.Equal("web-01", c.Results[0].Hostname) + suite.Equal(map[string]string{"group": "web", "env": "prod"}, c.Results[0].Labels) + suite.Empty(c.Results[0].Error) + + suite.Equal("web-02", c.Results[1].Hostname) + suite.Equal("timeout", c.Results[1].Error) + suite.Nil(c.Results[1].Labels) + }, + }, + { + name: "when minimal", + input: &gen.HostnameCollectionResponse{ + Results: []gen.HostnameResponse{ + {Hostname: "minimal-host"}, + }, + }, + validateFunc: func(c Collection[HostnameResult]) { + suite.Empty(c.JobID) + suite.Require().Len(c.Results, 1) + suite.Equal("minimal-host", c.Results[0].Hostname) + suite.Empty(c.Results[0].Error) + suite.Nil(c.Results[0].Labels) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := hostnameCollectionFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestNodeStatusCollectionFromGen() { + testUUID := openapi_types.UUID{ + 0x55, + 0x0e, + 0x84, + 0x00, + 0xe2, + 0x9b, + 0x41, + 0xd4, + 0xa7, + 0x16, + 0x44, + 0x66, + 0x55, + 0x44, + 0x00, + 0x00, + } + + tests := []struct { + name string + input *gen.NodeStatusCollectionResponse + validateFunc func(Collection[NodeStatus]) + }{ + { + name: "when all sub-types are populated", + input: func() *gen.NodeStatusCollectionResponse { + uptime := "5d 3h 22m" + disks := gen.DisksResponse{ + { + Name: "/dev/sda1", + Total: 500000000000, + Used: 250000000000, + Free: 250000000000, + }, + } + + return &gen.NodeStatusCollectionResponse{ + JobId: &testUUID, + Results: []gen.NodeStatusResponse{ + { + Hostname: "web-01", + Uptime: &uptime, + Disks: &disks, + LoadAverage: &gen.LoadAverageResponse{ + N1min: 0.5, + N5min: 1.2, + N15min: 0.8, + }, + Memory: &gen.MemoryResponse{ + Total: 8589934592, + Used: 4294967296, + Free: 4294967296, + }, + OsInfo: &gen.OSInfoResponse{ + Distribution: "Ubuntu", + Version: "22.04", + }, + }, + }, + } + }(), + validateFunc: func(c Collection[NodeStatus]) { + suite.Equal("550e8400-e29b-41d4-a716-446655440000", c.JobID) + suite.Require().Len(c.Results, 1) + + ns := c.Results[0] + suite.Equal("web-01", ns.Hostname) + suite.Equal("5d 3h 22m", ns.Uptime) + suite.Empty(ns.Error) + + suite.Require().Len(ns.Disks, 1) + suite.Equal("/dev/sda1", ns.Disks[0].Name) + suite.Equal(500000000000, ns.Disks[0].Total) + + suite.Require().NotNil(ns.LoadAverage) + suite.InDelta(0.5, float64(ns.LoadAverage.OneMin), 0.001) + suite.InDelta(1.2, float64(ns.LoadAverage.FiveMin), 0.001) + suite.InDelta(0.8, float64(ns.LoadAverage.FifteenMin), 0.001) + + suite.Require().NotNil(ns.Memory) + suite.Equal(8589934592, ns.Memory.Total) + suite.Equal(4294967296, ns.Memory.Used) + suite.Equal(4294967296, ns.Memory.Free) + + suite.Require().NotNil(ns.OSInfo) + suite.Equal("Ubuntu", ns.OSInfo.Distribution) + suite.Equal("22.04", ns.OSInfo.Version) + }, + }, + { + name: "when minimal", + input: &gen.NodeStatusCollectionResponse{ + Results: []gen.NodeStatusResponse{ + {Hostname: "minimal-host"}, + }, + }, + validateFunc: func(c Collection[NodeStatus]) { + suite.Empty(c.JobID) + suite.Require().Len(c.Results, 1) + + ns := c.Results[0] + suite.Equal("minimal-host", ns.Hostname) + suite.Empty(ns.Uptime) + suite.Empty(ns.Error) + suite.Nil(ns.Disks) + suite.Nil(ns.LoadAverage) + suite.Nil(ns.Memory) + suite.Nil(ns.OSInfo) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := nodeStatusCollectionFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestDiskCollectionFromGen() { + testUUID := openapi_types.UUID{ + 0x55, + 0x0e, + 0x84, + 0x00, + 0xe2, + 0x9b, + 0x41, + 0xd4, + 0xa7, + 0x16, + 0x44, + 0x66, + 0x55, + 0x44, + 0x00, + 0x00, + } + + tests := []struct { + name string + input *gen.DiskCollectionResponse + validateFunc func(Collection[DiskResult]) + }{ + { + name: "when disks are populated", + input: func() *gen.DiskCollectionResponse { + disks := gen.DisksResponse{ + { + Name: "/dev/sda1", + Total: 500000000000, + Used: 250000000000, + Free: 250000000000, + }, + { + Name: "/dev/sdb1", + Total: 1000000000000, + Used: 100000000000, + Free: 900000000000, + }, + } + + return &gen.DiskCollectionResponse{ + JobId: &testUUID, + Results: []gen.DiskResultItem{ + { + Hostname: "web-01", + Disks: &disks, + }, + }, + } + }(), + validateFunc: func(c Collection[DiskResult]) { + suite.Equal("550e8400-e29b-41d4-a716-446655440000", c.JobID) + suite.Require().Len(c.Results, 1) + + dr := c.Results[0] + suite.Equal("web-01", dr.Hostname) + suite.Empty(dr.Error) + suite.Require().Len(dr.Disks, 2) + suite.Equal("/dev/sda1", dr.Disks[0].Name) + suite.Equal(500000000000, dr.Disks[0].Total) + suite.Equal(250000000000, dr.Disks[0].Used) + suite.Equal(250000000000, dr.Disks[0].Free) + suite.Equal("/dev/sdb1", dr.Disks[1].Name) + }, + }, + { + name: "when empty", + input: &gen.DiskCollectionResponse{ + Results: []gen.DiskResultItem{ + {Hostname: "web-01"}, + }, + }, + validateFunc: func(c Collection[DiskResult]) { + suite.Empty(c.JobID) + suite.Require().Len(c.Results, 1) + suite.Equal("web-01", c.Results[0].Hostname) + suite.Nil(c.Results[0].Disks) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := diskCollectionFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestCommandCollectionFromGen() { + testUUID := openapi_types.UUID{ + 0x55, + 0x0e, + 0x84, + 0x00, + 0xe2, + 0x9b, + 0x41, + 0xd4, + 0xa7, + 0x16, + 0x44, + 0x66, + 0x55, + 0x44, + 0x00, + 0x00, + } + + tests := []struct { + name string + input *gen.CommandResultCollectionResponse + validateFunc func(Collection[CommandResult]) + }{ + { + name: "when all fields are populated", + input: func() *gen.CommandResultCollectionResponse { + stdout := "hello world\n" + stderr := "warning: something\n" + exitCode := 0 + changed := true + durationMs := int64(150) + + return &gen.CommandResultCollectionResponse{ + JobId: &testUUID, + Results: []gen.CommandResultItem{ + { + Hostname: "web-01", + Stdout: &stdout, + Stderr: &stderr, + ExitCode: &exitCode, + Changed: &changed, + DurationMs: &durationMs, + }, + }, + } + }(), + validateFunc: func(c Collection[CommandResult]) { + suite.Equal("550e8400-e29b-41d4-a716-446655440000", c.JobID) + suite.Require().Len(c.Results, 1) + + cr := c.Results[0] + suite.Equal("web-01", cr.Hostname) + suite.Equal("hello world\n", cr.Stdout) + suite.Equal("warning: something\n", cr.Stderr) + suite.Empty(cr.Error) + suite.Equal(0, cr.ExitCode) + suite.True(cr.Changed) + suite.Equal(int64(150), cr.DurationMs) + }, + }, + { + name: "when minimal with error", + input: func() *gen.CommandResultCollectionResponse { + errMsg := "command not found" + exitCode := 127 + + return &gen.CommandResultCollectionResponse{ + Results: []gen.CommandResultItem{ + { + Hostname: "web-01", + Error: &errMsg, + ExitCode: &exitCode, + }, + }, + } + }(), + validateFunc: func(c Collection[CommandResult]) { + suite.Empty(c.JobID) + suite.Require().Len(c.Results, 1) + + cr := c.Results[0] + suite.Equal("web-01", cr.Hostname) + suite.Equal("command not found", cr.Error) + suite.Equal(127, cr.ExitCode) + suite.Empty(cr.Stdout) + suite.Empty(cr.Stderr) + suite.False(cr.Changed) + suite.Zero(cr.DurationMs) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := commandCollectionFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestDNSConfigCollectionFromGen() { + testUUID := openapi_types.UUID{ + 0x55, + 0x0e, + 0x84, + 0x00, + 0xe2, + 0x9b, + 0x41, + 0xd4, + 0xa7, + 0x16, + 0x44, + 0x66, + 0x55, + 0x44, + 0x00, + 0x00, + } + + tests := []struct { + name string + input *gen.DNSConfigCollectionResponse + validateFunc func(Collection[DNSConfig]) + }{ + { + name: "when all fields are populated", + input: func() *gen.DNSConfigCollectionResponse { + servers := []string{"8.8.8.8", "8.8.4.4"} + searchDomains := []string{"example.com", "local"} + + return &gen.DNSConfigCollectionResponse{ + JobId: &testUUID, + Results: []gen.DNSConfigResponse{ + { + Hostname: "web-01", + Servers: &servers, + SearchDomains: &searchDomains, + }, + }, + } + }(), + validateFunc: func(c Collection[DNSConfig]) { + suite.Equal("550e8400-e29b-41d4-a716-446655440000", c.JobID) + suite.Require().Len(c.Results, 1) + + dc := c.Results[0] + suite.Equal("web-01", dc.Hostname) + suite.Empty(dc.Error) + suite.Equal([]string{"8.8.8.8", "8.8.4.4"}, dc.Servers) + suite.Equal([]string{"example.com", "local"}, dc.SearchDomains) + }, + }, + { + name: "when minimal", + input: &gen.DNSConfigCollectionResponse{ + Results: []gen.DNSConfigResponse{ + {Hostname: "web-01"}, + }, + }, + validateFunc: func(c Collection[DNSConfig]) { + suite.Empty(c.JobID) + suite.Require().Len(c.Results, 1) + suite.Equal("web-01", c.Results[0].Hostname) + suite.Nil(c.Results[0].Servers) + suite.Nil(c.Results[0].SearchDomains) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := dnsConfigCollectionFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestDNSUpdateCollectionFromGen() { + tests := []struct { + name string + input *gen.DNSUpdateCollectionResponse + validateFunc func(Collection[DNSUpdateResult]) + }{ + { + name: "when all fields are populated", + input: func() *gen.DNSUpdateCollectionResponse { + changed := true + + return &gen.DNSUpdateCollectionResponse{ + Results: []gen.DNSUpdateResultItem{ + { + Hostname: "web-01", + Status: gen.DNSUpdateResultItemStatus("applied"), + Changed: &changed, + }, + }, + } + }(), + validateFunc: func(c Collection[DNSUpdateResult]) { + suite.Require().Len(c.Results, 1) + + dr := c.Results[0] + suite.Equal("web-01", dr.Hostname) + suite.Equal("applied", dr.Status) + suite.True(dr.Changed) + suite.Empty(dr.Error) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := dnsUpdateCollectionFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestPingCollectionFromGen() { + tests := []struct { + name string + input *gen.PingCollectionResponse + validateFunc func(Collection[PingResult]) + }{ + { + name: "when all fields are populated", + input: func() *gen.PingCollectionResponse { + packetsSent := 5 + packetsReceived := 5 + packetLoss := 0.0 + minRtt := "1.234ms" + avgRtt := "2.345ms" + maxRtt := "3.456ms" + + return &gen.PingCollectionResponse{ + Results: []gen.PingResponse{ + { + Hostname: "web-01", + PacketsSent: &packetsSent, + PacketsReceived: &packetsReceived, + PacketLoss: &packetLoss, + MinRtt: &minRtt, + AvgRtt: &avgRtt, + MaxRtt: &maxRtt, + }, + }, + } + }(), + validateFunc: func(c Collection[PingResult]) { + suite.Require().Len(c.Results, 1) + + pr := c.Results[0] + suite.Equal("web-01", pr.Hostname) + suite.Equal(5, pr.PacketsSent) + suite.Equal(5, pr.PacketsReceived) + suite.InDelta(0.0, pr.PacketLoss, 0.001) + suite.Equal("1.234ms", pr.MinRtt) + suite.Equal("2.345ms", pr.AvgRtt) + suite.Equal("3.456ms", pr.MaxRtt) + suite.Empty(pr.Error) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + result := pingCollectionFromGen(tc.input) + tc.validateFunc(result) + }) + } +} + +func (suite *NodeTypesTestSuite) TestDerefHelpers() { + suite.Run("derefString", func() { + s := "hello" + suite.Equal("hello", derefString(&s)) + suite.Equal("", derefString(nil)) + }) + + suite.Run("derefInt", func() { + i := 42 + suite.Equal(42, derefInt(&i)) + suite.Equal(0, derefInt(nil)) + }) + + suite.Run("derefInt64", func() { + i := int64(42) + suite.Equal(int64(42), derefInt64(&i)) + suite.Equal(int64(0), derefInt64(nil)) + }) + + suite.Run("derefFloat64", func() { + f := 3.14 + suite.InDelta(3.14, derefFloat64(&f), 0.001) + suite.InDelta(0.0, derefFloat64(nil), 0.001) + }) + + suite.Run("derefBool", func() { + b := true + suite.True(derefBool(&b)) + suite.False(derefBool(nil)) + }) + + suite.Run("jobIDFromGen", func() { + id := openapi_types.UUID{ + 0x55, + 0x0e, + 0x84, + 0x00, + 0xe2, + 0x9b, + 0x41, + 0xd4, + 0xa7, + 0x16, + 0x44, + 0x66, + 0x55, + 0x44, + 0x00, + 0x00, + } + suite.Equal("550e8400-e29b-41d4-a716-446655440000", jobIDFromGen(&id)) + suite.Equal("", jobIDFromGen(nil)) + }) +} + +func TestNodeTypesTestSuite(t *testing.T) { + suite.Run(t, new(NodeTypesTestSuite)) +} diff --git a/pkg/osapi/response.go b/pkg/osapi/response.go new file mode 100644 index 0000000..8ee4e96 --- /dev/null +++ b/pkg/osapi/response.go @@ -0,0 +1,93 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + "fmt" + + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +// Response wraps a domain type with raw JSON for CLI --json mode. +type Response[T any] struct { + Data T + rawJSON []byte +} + +// NewResponse creates a Response with the given data and raw JSON body. +func NewResponse[T any]( + data T, + rawJSON []byte, +) *Response[T] { + return &Response[T]{ + Data: data, + rawJSON: rawJSON, + } +} + +// RawJSON returns the raw HTTP response body. +func (r *Response[T]) RawJSON() []byte { + return r.rawJSON +} + +// checkError inspects the HTTP status code and returns the appropriate +// typed error. For success codes (200, 201, 202, 204) it returns nil. +// The variadic responses are the parsed error body pointers from the +// generated response struct (e.g., resp.JSON400, resp.JSON401, etc.). +func checkError( + statusCode int, + responses ...*gen.ErrorResponse, +) error { + switch { + case statusCode >= 200 && statusCode < 300: + return nil + } + + msg := extractErrorMessage(statusCode, responses...) + + switch statusCode { + case 400: + return &ValidationError{APIError{StatusCode: statusCode, Message: msg}} + case 401, 403: + return &AuthError{APIError{StatusCode: statusCode, Message: msg}} + case 404: + return &NotFoundError{APIError{StatusCode: statusCode, Message: msg}} + case 500: + return &ServerError{APIError{StatusCode: statusCode, Message: msg}} + default: + return &UnexpectedStatusError{APIError{StatusCode: statusCode, Message: msg}} + } +} + +// extractErrorMessage finds the first non-nil error message from the +// response pointers, or falls back to a generic message. +func extractErrorMessage( + statusCode int, + responses ...*gen.ErrorResponse, +) string { + for _, r := range responses { + if r != nil && r.Error != nil { + return *r.Error + } + } + + return fmt.Sprintf("unexpected status %d", statusCode) +} diff --git a/pkg/osapi/response_public_test.go b/pkg/osapi/response_public_test.go new file mode 100644 index 0000000..0ac76a9 --- /dev/null +++ b/pkg/osapi/response_public_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-sdk/pkg/osapi" +) + +type ResponsePublicTestSuite struct { + suite.Suite +} + +func (suite *ResponsePublicTestSuite) TestRawJSON() { + tests := []struct { + name string + rawJSON []byte + validateFunc func(*osapi.Response[string]) + }{ + { + name: "when RawJSON returns the raw bytes", + rawJSON: []byte(`{"hostname":"web-01"}`), + validateFunc: func(resp *osapi.Response[string]) { + suite.Equal( + []byte(`{"hostname":"web-01"}`), + resp.RawJSON(), + ) + }, + }, + { + name: "when RawJSON returns nil for empty response", + rawJSON: nil, + validateFunc: func(resp *osapi.Response[string]) { + suite.Nil(resp.RawJSON()) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + resp := osapi.NewResponse("test", tc.rawJSON) + tc.validateFunc(resp) + }) + } +} + +func (suite *ResponsePublicTestSuite) TestData() { + tests := []struct { + name string + data string + rawJSON []byte + validateFunc func(*osapi.Response[string]) + }{ + { + name: "when Data contains the domain type", + data: "web-01", + rawJSON: []byte(`{"hostname":"web-01"}`), + validateFunc: func(resp *osapi.Response[string]) { + suite.Equal("web-01", resp.Data) + }, + }, + { + name: "when Data contains an empty string", + data: "", + rawJSON: []byte(`{}`), + validateFunc: func(resp *osapi.Response[string]) { + suite.Empty(resp.Data) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + resp := osapi.NewResponse(tc.data, tc.rawJSON) + tc.validateFunc(resp) + }) + } +} + +func TestResponsePublicTestSuite(t *testing.T) { + suite.Run(t, new(ResponsePublicTestSuite)) +} diff --git a/pkg/osapi/response_test.go b/pkg/osapi/response_test.go new file mode 100644 index 0000000..b5ad950 --- /dev/null +++ b/pkg/osapi/response_test.go @@ -0,0 +1,279 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package osapi + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-sdk/pkg/osapi/gen" +) + +type ResponseTestSuite struct { + suite.Suite +} + +func (suite *ResponseTestSuite) TestCheckErrorSuccess() { + tests := []struct { + name string + statusCode int + }{ + { + name: "when status is 200", + statusCode: 200, + }, + { + name: "when status is 201", + statusCode: 201, + }, + { + name: "when status is 202", + statusCode: 202, + }, + { + name: "when status is 204", + statusCode: 204, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + err := checkError(tc.statusCode) + suite.NoError(err) + }) + } +} + +func (suite *ResponseTestSuite) TestCheckErrorValidation() { + tests := []struct { + name string + statusCode int + validateFunc func(error) + }{ + { + name: "when status is 400", + statusCode: 400, + validateFunc: func(err error) { + var target *ValidationError + suite.True(errors.As(err, &target)) + suite.Equal(400, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + err := checkError(tc.statusCode) + suite.Error(err) + tc.validateFunc(err) + }) + } +} + +func (suite *ResponseTestSuite) TestCheckErrorAuth() { + tests := []struct { + name string + statusCode int + validateFunc func(error) + }{ + { + name: "when status is 401", + statusCode: 401, + validateFunc: func(err error) { + var target *AuthError + suite.True(errors.As(err, &target)) + suite.Equal(401, target.StatusCode) + }, + }, + { + name: "when status is 403", + statusCode: 403, + validateFunc: func(err error) { + var target *AuthError + suite.True(errors.As(err, &target)) + suite.Equal(403, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + err := checkError(tc.statusCode) + suite.Error(err) + tc.validateFunc(err) + }) + } +} + +func (suite *ResponseTestSuite) TestCheckErrorNotFound() { + tests := []struct { + name string + statusCode int + validateFunc func(error) + }{ + { + name: "when status is 404", + statusCode: 404, + validateFunc: func(err error) { + var target *NotFoundError + suite.True(errors.As(err, &target)) + suite.Equal(404, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + err := checkError(tc.statusCode) + suite.Error(err) + tc.validateFunc(err) + }) + } +} + +func (suite *ResponseTestSuite) TestCheckErrorServer() { + tests := []struct { + name string + statusCode int + validateFunc func(error) + }{ + { + name: "when status is 500", + statusCode: 500, + validateFunc: func(err error) { + var target *ServerError + suite.True(errors.As(err, &target)) + suite.Equal(500, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + err := checkError(tc.statusCode) + suite.Error(err) + tc.validateFunc(err) + }) + } +} + +func (suite *ResponseTestSuite) TestCheckErrorUnexpected() { + tests := []struct { + name string + statusCode int + validateFunc func(error) + }{ + { + name: "when status is 503", + statusCode: 503, + validateFunc: func(err error) { + var target *UnexpectedStatusError + suite.True(errors.As(err, &target)) + suite.Equal(503, target.StatusCode) + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + err := checkError(tc.statusCode) + suite.Error(err) + tc.validateFunc(err) + }) + } +} + +func (suite *ResponseTestSuite) TestCheckErrorWithMessage() { + tests := []struct { + name string + statusCode int + responses []*gen.ErrorResponse + validateFunc func(error) + }{ + { + name: "when error response contains a message", + statusCode: 400, + responses: func() []*gen.ErrorResponse { + msg := "field 'name' is required" + return []*gen.ErrorResponse{{Error: &msg}} + }(), + validateFunc: func(err error) { + suite.Contains(err.Error(), "field 'name' is required") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + err := checkError(tc.statusCode, tc.responses...) + suite.Error(err) + tc.validateFunc(err) + }) + } +} + +func (suite *ResponseTestSuite) TestCheckErrorNilResponses() { + tests := []struct { + name string + statusCode int + responses []*gen.ErrorResponse + validateFunc func(error) + }{ + { + name: "when all responses are nil", + statusCode: 400, + responses: []*gen.ErrorResponse{nil, nil}, + validateFunc: func(err error) { + suite.Contains(err.Error(), "unexpected status 400") + }, + }, + { + name: "when no responses are provided", + statusCode: 500, + responses: nil, + validateFunc: func(err error) { + suite.Contains(err.Error(), "unexpected status 500") + }, + }, + { + name: "when response has nil Error field", + statusCode: 404, + responses: []*gen.ErrorResponse{{Error: nil}}, + validateFunc: func(err error) { + suite.Contains(err.Error(), "unexpected status 404") + }, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + err := checkError(tc.statusCode, tc.responses...) + suite.Error(err) + tc.validateFunc(err) + }) + } +} + +func TestResponseTestSuite(t *testing.T) { + suite.Run(t, new(ResponseTestSuite)) +}