@@ -155,6 +155,97 @@ Stop the agent from accepting new jobs. In-flight jobs continue to completion.
+
+
+
+ Invalid hostname.
+
+
+
+
+
+
+
+
+
+
+ Schema
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/docs/gen/api/get-agent-details.api.mdx b/docs/docs/gen/api/get-agent-details.api.mdx
index 7aa73a3a..5e78940f 100644
--- a/docs/docs/gen/api/get-agent-details.api.mdx
+++ b/docs/docs/gen/api/get-agent-details.api.mdx
@@ -5,7 +5,7 @@ description: "Get detailed information about a specific agent by hostname."
sidebar_label: "Get agent details"
hide_title: true
hide_table_of_contents: true
-api: eJztWV9v2zgS/yoEn24BxZGSOJvqLZsmvdzttkGaYB+KwKDFsc2NRKrkKK3P8Hc/DCnLkq0kWmT3YYEiD7HN4W/+kzPDFZfgMqtKVEbzlH8AZBJQqBwkU3pmbCFoiYmpqZAJ5krI1ExlTMxBI5su2cI41KKAEY+4KcF6+mvJUz4HPCeq9x7Q8YijmDuefuH+58lvQos5FPTx/OZ64hEnDYTjDxF3kFVW4ZKnX1b8FxAW7HmFC8Lw5KkFIfnD+iHipbCiAATrPDGJxFO+kY5HXJGCpcAFj7iFr5WyIHmKtoKIu2wBheDpiuOypH0OrdJzvo52DHS3gEZjZmYMF1CbAg2zgFbBE4w4CWTBlUY7cAR7FMf0rwvmzVDb25H9MqMRNBKlKMtcZd4Sh384Il/tS2mmf0CGPOKlJbuhCswapff0GazOiFR3KLByfSigq4KccAtCLnnEPxoMHx/6OGSVtaRpwNvnk4sp5K5PLyGlIiCR33Q0fMVL/4XlwZPIK2ABmmVGz9S8siCZ0TvcLcyVQ7AgJwL7lA1ZwFMuBcIBKh9NXYa/L6AFy3LhkFmYWXALSiR0bAHC4hREY1mLfyHD0poMnGM1rudh3IQyuM+sXaxPIeX0nLmlQyjaiT/aCy6pSM5pFTYPCbB2cP2qdPWdtTGIA3wXRZkTzP200liR+E9g3WAWNfFQLkfxKD7h63X7IPjS1WwrwEPEUaHf9+nztZ6Z2zqxScqq9O4ZJGRt3bAlBL4RciKewIo5vO6nFgZtZPVGx2bGsiRi44gJLVkyZoXSFYLbd15SqLZFdVVMwe4x+rWF7sHJpj6mkxq6Y854dHy0jvj4jdgdsVvgR2friCdvRU+ehR/vxoE3Uq3PhnMrCIjHeWDRjoQCCmOXr3vxN0/HKkcivphpaFDkLUClEeY9St8RHQv8mdJsutzV8uz47Ow0JjvOLMAAyCsL8CLiUfzu52RMbq8cWe1VxHsH8kXEk+TdyXF8suuMYIRa8JpbyxnBmm0/CJstFEKGlR2Qlxc396y9o3tOiEKenhDoI1gN+WTwmfTpMwtbNidTF3c8Ssaj+OBdcjAHDVZlxCMrq0lmKo0DrPnRhz8dd7mZq0zk7OLmfsee5O2vcoCwV1WeL9nXSuRqpkAyaQqhNNsUdVuxv8H0IE5G9Q+jzBT+LgP7pDKYFHP7Oq9rrbA+xLrY4TdJgKXIHsV8IOBNIGaFLybtjv9KJECyop2JrFM7CGsFlS4KoeitPbrJ+Fw9tWUGuIg9t/Lp5GXK5N3RKDk9GyWjZLPj9OUdMziL0zTx54zIXqaN4zRJ0qOj9Pg4PTlJx2PaNROFypcD/HPDhJSWaomwpWtQWflk3JR/SgP6yhrwlKBo9WE3g73lWin7EfCbsY/XG6dsk5d8b1Uh7HLSuOx1kT+2yotmG6ODorkBJMxElSOzZvfuarzml/5UfOzU8kwHvTwTKqZQTHNgoNEue6oocKi0GFZEvd8SN1xqL3WViUf+j/SZC4RvYoDHPwTCfsBWpHYSaYBXajm3Htk/Tza2L4R7HI5I1HSFXFy/v2XaoNiv7w4DLLVj2YDT9JZ8zwJ5ByiJY8qd3Letr8kXUDzxjlfi+Hi/2GwFwNZbbRO3UsZDty+5fsvsR56iS9anf4Z/osEKTXGXyeV3BC0pqQiLzawp2HY3dSBPSoJ1TefYf1zuNI7vrVA6LF0YK40O13tfn0zdr6xy36cQvGeUGR1EeMPRHlaflzXUGDeUHVRTRPzfar6gKpDkV+6xWXno65mnxuQgdOg0hevLd98CO5ygFdp5bSbPdRU9reFexURbGkGegW6fx0bCxcaM+8F1SY20QJBMGwlsa3BvfwLLlYY3WF8V4FAU5VB1Iw5PoHsa53X0wuTDnwfOdRut7RpYa3qqjT3bNsJupGgZ8q62xaVf2DNkHcUUumzrDrZQDg1dEbu8WqOr2pctXh6MmlG+XtPGkzjZny/da1Hhwlj1P5DsgJ3fXLNHWLKGyV82cHrGevt3ZOv75sr2exkuBDKT+UmR7B6fV2Ea2Zqv1YOkUbBxGC6+znx7VtV76qFmI0QvW1kBsd5cuxQApsL67JFDeqm7Rkna0GEyjuP1euvUS6JqlUPBscf7jr0ydqqkBM0O2LV21WymMuXnQGAL5Zw/D39495/g3ZPnxsLaIJuZSv9I03+CI8d9831PuDEH1S3ibxr4/3Ds3+TY0EUsTP2cRYanB6SUH3pXHq42l/Sah0FIeH5qvVx9JhcGL7XfrxqpF4hUTHhP+3LRE/Go/nC1KYD+8/udrxCakX67EGDbpzS65VvD65Qnm46wNA4LobfDDP/a1wnJXduttgH61qfBWl2E73hY5kL5SrOyfr4ZbFo/6fGIp03p8xBKOlpcrabCwb3N12v6+WsFNG0lUz8Jq6jj8e9+Ujn6LHk6E7nb7WLaCv3rtq6DfmIDX/We0WJT92qqev2zE085p6Hhsv0Cuab2YAFCgvWShuXzLIMSWxv3zgF6S2yC8MPlHXVt3RjaiRmP3ivUahUo7swj6PW6kRHpOwm4Xv8fAFNaBg==
+api: eJztWd9v2zgS/lcIPt0BsiM5djYVsA+5NOn5rtsGaYJ9KAKDFsc2NxKpkpQbn6H//TCkLEu2kmiR3YcCRR5im8NvfnxDcobcUg4m0SK3Qkka0w9gCQfLRAqcCLlQOmM4RNhcFZYwYnJIxEIkhC1BWjLfkJUyVrIMhjSgKgft5KecxnQJ9gKl3jtAQwNq2dLQ+Ct1P89+Y5ItIcOPFzfTmUOc1RCGPgTUQFJoYTc0/rql/wKmQV8UdoUYTjzWwDh9KB8CmjPNMrCgjRNGk2hMd9bRgAp0MGd2RQOq4VshNHAaW11AQJ8GiuVikCgOS5ADeLKaDby1W7pmqeDMItxuXpAJ+WsUZOzp19FkQsuAmmQFGUNxu8lR1Fgt5JIGNBPyI8glWh0FNGNPu2+jyaQMDgi4W0EdUaIWxK6gCrVVRIPVAtYwpOiwBpMracDZOApD/NcGc2Gu+DTIT6KkBWlRkuV5KhIX6ZM/DIpvj31Q8z8gsTSguUZerPDK6qB2eNvTnaGLmWW2MF0oIIsMSb4Fxjc0oJ+U9R8fujQkhdboqcc71pOyOaSmyy/GuUAglt60PGzbc8TSf2EzWLO0AOKhSaLkQiwLDZwoeaBdw1IYCxr4jNkuZ/0qozHFJBtY4bK1rfD3FTRgScqMJRoWGswKF6o1ZAVM2zmwOrLa/oUKc60SMIZUuE6HMjPcIbrC2sb67Je0XBKzMRay5sYyPEouLtDOeeEn90mwZnJ9FLJ4Ik0M1ABPLMtThLmfF9IWaP4atOmtohLuq2UUDsMxLcvmRvO17dnegIeAWmHdvM9fpnKhbquFjVYWuaOnl5FVdP0Un/iK8Rlbg2ZLeJ2nBgZOJNVEQxZKkyggk4AwyUk0IZmQhQVzTF6UiWZEZZHNQR8p+thAd+AYU5fTUQXdCmc4PB2VAZ28EbtldgN8dF4GNHorevQs/OQwD1yQKn92mhtJgDouvIpmJmSQKb15ncXfnBwpDJr44kqzyrK0ASikhWWH03coR7x+IiSZbw69PD89Pz8LMY4LDdAD8loDvIg4Ct/9Ek2Q9sJg1F5FvDfAX0QcR+/Gp+H4kAwfhMrwSluDDB/NJg9MJythIbGF7rEuL2/uSXNGe59gGT8bI+gjaAnprPee9PkL8VN2O1MbdzKMJsNw8C4aLEGCFgnqSPJilqhC2h7R/OTSH7e7VC1FwlJyeXN/EE9k+xvvYex1kaYb8q1gqVgI4ISrjAlJdkXj3uzvMB+E0bD6YZiozJ1loNcigVm21K/rmkphq02sje1/4wiYs+SRLXsC3nhhkrliVR/wl1sExCjqBUtatQPTmmHpIixknbVHezE+V0/tlYFdhU5bvh6/LBm9Gw2js/NhNIx2M85enrGA8zCOI7fPsORl2TCMoygejeLT03g8jn35u2CZSDc9+LkhjHONtYSf0g4oL9xi3JV/QoJ1lTvYM4TC0YfDFewi11iyn8B+V/pxuiNlv3iRey0ypjezmrLXTf7UKC/qaQQ3ivoE4LBgRWqJVodnV82aG/pT+XFQyxPp/XJKsJiybJ4CAWn1pqOKAmOFZP2KqPd74VpLxVLbmXDo/tCfJbPwnfVg/IMX7AZsZGprIfVgpbJzz8jxfrKLfcbMY39ElMYj5HL6/pZIZdlxfXfiYbEdS3rsprfIPfHiLaAoDHHtpFWj+bJ9HsUJH7AShqfHxWYjAfZsNUPcWDIOunnIdUfmOPMEHrJu+Sf2TzRYvuluK7l6siA5LirEIgutMrKfjR3IWnDQpu4cu7fLg8bxvWZC+qFLpbmS/njv6pOx++VF6voUhHeKEiW9CW/Y2v3o87b6GuMGVwfWFAH9t1iusApE+4V5rEceunrmuVIpMOk7TWa61rtrgY2dWc2kcd7MnusqOlrDo4oJp9SGPAPd3I8Vh8tdGI+T6wobaWaBE6k4kH3AXfwRLBUS3hB9kYGxLMv7uhtQWIPsaJzL4IWbD7cfGNNutPZjoLXqqDaOYlsbu7OiEci7KhZXbuAokFUWY+qSPR1kJYxVeEQc6mpcjVVcNnQ5MGxGaVnixHHX/dJUuqux1hXgX3TF9Ey8jk/FxvfdIe3mErtilqjE3Q3x9oZ57e83Gzdq1dXR0EfVX1e+rny/O1VzqmvS2ohOtbwAVL07aJFyVdhqt+F9uqe72kmc0FIyCcOy3NN4hVKNAshTGR1TeS9ZYVdKi/8BJwNycTMlj7Ahdb78JPZHIPb0mNhrpeeCc5BkQKbSFIuFSIS70gOdCWPc0faT3R+B3fFzN/xSWbJQhfy5TH8EIiddR6kT3IUDS1D2N73d/CT2byLWN4QrVb18YuDxrTGmJ47Kk+2uSiqpv9PyL5WNR84vSKFnqfnUWVu9shbrQse0q/ydEA2qD9e7WvY/v9+5Yq9+nWnWdGT/6oqnfOMdIqbRrrnPlbEZk/t7Kfcw3ErJw9ht9wn61lfkyl0LT/YkT5lwTUOh3VW1j2n1+ksDGtdV7IOvznFwu50zA/c6LUv8+VsBeHGOoV4zLbB5dU/EXBj8zGm8YKk5bEibDv3jtqqD/kl6PtA+48WuhZHYwLgXRBpTive/m+ZjdYmd3goYB+0s9cMXSQK5bUw82gfwWbhOwg9Xd9iAt3PoIGcceqdR262XuFOPIMuyttHidzSwLP8PPFIaHA==
sidebar_class_name: "get api-method"
info_path: gen/api/agent-management-api
custom_edit_url: null
@@ -68,7 +68,7 @@ Get detailed information about a specific agent by hostname.
@@ -916,6 +916,97 @@ Get detailed information about a specific agent by hostname.
+
+
+
+ Invalid hostname.
+
+
+
+
+
+
+
+
+
+
+ Schema
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/docs/gen/api/list-jobs.api.mdx b/docs/docs/gen/api/list-jobs.api.mdx
index 0943ec3c..9392d434 100644
--- a/docs/docs/gen/api/list-jobs.api.mdx
+++ b/docs/docs/gen/api/list-jobs.api.mdx
@@ -5,7 +5,7 @@ description: "Retrieve jobs, optionally filtered by status."
sidebar_label: "List jobs"
hide_title: true
hide_table_of_contents: true
-api: eJztWF+P2zYM/yqCnlbASXNdOwwB+pCtve2Kbju0d9hDdwhom050Z0s+iUovCwzsQ+wT7pMMlGLn/+VatEMH9CmxTVE/8kdSFBcyR5dZVZMyWg7lGySrcIbi2qQuESa8h7Kci0KVhBZzkc6FIyDv+jKRBBMnh+/kK5OOfwENE6xQ03h0fja+NunY1GiBVTh5lUiHmbeK5nL4biF/QLBoR56mvP7apEOLkMur5iqRNViokNC6IKqhQjmUcVeZSMVIbz3auUykxVuvLOZyWEDpMJF3PQO16mUmxwnqHt6RhV7EuZAzKFUOxOpMpQirmuaJ0WiK586nlSLCXNTWZOic0hORmaoukV8WoEr+BpYUlGN+9BZlk0iXTbEC1k7zOgK1Sk9kIlH7iq3rVMtErpTLRHbqZSLjBjJYv7HFVZNssXQauAgcrdPRJJ2vSlUp+gyuqpR+PthrtNKEE7QykZXSqmLDB4y7AF+SHJ4Mdqz4Be5YTmhfpWiFKaJBZIRF8lb3xaVDMRCFsUIbEUzaMNIUhcMvyspdI3/dMc7dqDrYVMNE6ZAdfclRb9HVRjsMuJ4MBvyzqWwkSuWoVdYPAaQJNbEo1HWpsqDv8bVj+cUufpNeY0YxDGu0pOJuZAjKMZvv9hq7CeOCpbdpq4CyKacMTXFZLBgg3gGHuBw+fcL+DJE6zozX5PbBgjxXseScbwLcRLTj5h9ZI2OBstzKC/FNl37JWmonq9xOlsmdbGf3o754gVbNOP2tqcQNzgXHnhO5t62tTAn/r8E58c9ff3OshhgTXNBc/w/NcLd9C9YCR+v2+wMEqXxffSmMrYDkUHqv8h2aLrW69ShUjppUoSJXjPjapH3ZsbFP8ZZzvbWoqfXnlpbMInAFO66GBZXRglSFjqCqw/rujNjnha3Am6LoxEUOBP2DMUPWYxOSKmTmdiqxqvgt6BGqWMVDgIXWGnvcqJcsJip0DibIWmIoBRVT4yiWqmNaRhP2bysvaArUhirmG972NVerfAx0XOtrcCTigi2fb5Sa+31+jrYHAV67KDqMK1hqDeQZ79LWoyP5eyC6DwUipzkQ7CmELaCWv/sou5eJpmmaRAYDxwzjw1yy1imExf+VVw5bmvudZNqwlOOgVBr3lKKtbJ1ao01pJiqDUji89agzXJZ7UaoCs3lWosAZagpWPrCUdZF4PIDP3v4mvv9ucLKK3rb4hF0j6bPl+XckT1lMsMzGeQDZjTbvS8wn/OQIbHi952iwoTPOH31UZhsrnPE2W6b2BDUXsWVqr2xZlpHjWn/2Fegeny+QlijWPu5x0IcUshwJVOm4kC37ibTEfoidEDzERzl3+y+C4JtlSZAbn18rR69M6ta/Nol8uq+lOdOhAxPctaEjsWr9P2F780AXjPY6Mvgl0GayjE/CfKOvkafxYhDb1nh5WjXkcunQB2ze1YiOBEiNpxWIvdvmHnlrjfTe2JuQKMZH4rnhfUg31xnJCzY2eTYYrPMaQmSH1JNdUi81eJoaq/7EXPTE6PwstE5dZ/6V2P8Dsd/uEntqbKryHLXoiTPtfFGoTIWDEG2lnAs3/a/sfvnsPttXi+MR0N5nPvEV8yuZn4nM0DfQ1ORyKCdhHFIDD9Xk42uTSp662VmcpK2N4N4ya5GY9UFcB3RKVMvl/IOf0yAkk+Wf0/bi+er3C8kIlC5MbO4izNj2rCaCfAbIRDKQaPFJf9API5baOKoghNJyrMPtQ4i+bTctVrH44YPKaBfhHT2uS1DhUu5tyVqjv8IQkseU3Nzx42KRgsNLWzYNv47zJZ5H5spxW7SaMB1EeWBQdwDODc7Xx5wzKD0LhZHXw/f8uLHavZDaaeJHInrgDOxeCN2sb4Xhih+sYhAc3NyWI+RoA0dx1SjLsF5ftVPBWEuXPj+9vODb2mYqbIV+0N7enPR8TfdiESUuzA3qppEtdOJn2Vw1TfMvwukvHg==
+api: eJztWG1v20YM/iuH+9QCsmOn7TAY6IdsbbYU7Ra0KfahCwxKouxLpDvljnLjGQL2I/YL90sGnl4s+SV2i3bYgH6yJfF4JB/yOR5XMkYXWZWTMlpO5Fskq3CB4saELhDGv4c0XYpEpYQWYxEuhSOgwg1lIAlmTk4+yFcmnL4BDTPMUNP07PJiemPCqcnRAqtw8jqQDqPCKlrKyYeV/AHBoj0raM7rb0w4sQixvC6vA5mDhQwJrfOiGjKUE1ntKgOp2NK7Au1SBtLiXaEsxnKSQOowkPcDA7kaRCbGGeoB3pOFQWXnSi4gVTEQqzOZIsxyWgZGo0meuyLMFBHGIrcmQueUnonIZHmK/DIBlfI3sKQgnfJjYVGWgXTRHDNg7bTMK0Ot0jMZSNRFxt61qmUg18plIFv1MpDVBtJ739viugw2UDr3WHiMunCUQRurVGWKvkKoMqWfj4MM7p+PR6OdzitNOEMrA5kprTIOwDiQGdzX/0cj9iaBIiV+2vLtTSUpdJGFaIVJKjdztCKHGYpH48F4NHrc89YkicOv5e7RbnYd2/brl74/ZIS7VblIjPdLaV8mQ8npb9HlRjv0dp2ORvzTV3YmUuWoUTb0maQJNbEo5HmqIq/v5Max/GrbfhPeYERVPuZoSVW7kSFIp+y+2+ls34wrlt5EKgOK5lw7NMeaNdhAvAfOdTl5esrx9Ck7jUyhye0yC+JYVdxz2Tewb9FWmH9kjWwLpOlGgYhHbR0GnRoP1kUe1FUebJb546F4gVYtmAesycQtLgXnnhNxYRtfGRL+n4Nz4u8//xLaCJ9jgpnNDX/X3TCsOqwwedbjhclplxkmT0YtOUyebNPD5LQsA7kJGVgLXASb7/fgruJd/JUYmwHJiSwKFW+h/16ruwKFilGTSlSVAhyIGxMOZQvyLsUbmBXWoqYGpg0tkUXwcTiohgWV0YJUho4gy/369gzaFYWNfJ6jaMVFDATDvalItsDS16ov+M0KZVXVN69HqGSdZt4stNbYw069ZDGRoXNMfiqpM9SrmBtHFQMe0nI24/g28oLmQE0FYNyLdpEzCcZToMNaX4MjUS3YiHmPwR6O+SXaAXjzmkVVwJgYQ2sgjniXhuYO0MKe7N6XiMweQLCDXxuDGvweguxBJMqSq9M7OGUzPi0knU7EL/63orLf07jYKqaep5wHqdK4g4o2qnVujTapmakIUuHwrkAdYX2KiFQlGC2jFAUuUJP38kgqazPxcAJfvPtVfP/daLzO3oZ8/K4V6Iv6WD1QpywmWKZ3zEB0q83HFOMZPzkC61/vOHGs77zjx59V2cYKZwob1aU9Q80kVpf22peaRg5r/bnIQA/42IIwRdH5uCNAn0JkMRKo1DGR1W1KmOLQ545PHuKjkW8TL7zg25oSZO/za+XolQld92sZyKe7OqUL7Rs7wc0gOhLrq8UX7JqODMHZzkD6uHjYTBTxSRj32iV5Xl08yNQpssBOwy/rgB6xecsRLQgQmoLWRuzcNi6Qt9ZIH4299YViigp47qOPaRJbJ3lBb5Nno1EXV58iW6COt0F9r6GgubHqD4zFQJxdXviOrG34vwH7fwD2yTaw58aGKo5Ri4G40K5IEhUpfxCizZRzfpLwDd3/PrrPdnFxdQQ016QvfHP9BuZXAtP3DTQ3sZzImZ+y5MBDO3lyY0LJUz27qCZ1nRHfO0atAqY76GsNnRPlsh6r8HPohWRQ/zlvLp6vfruS/nqrE1M1d5WZVduznjjyGSADyYZUHo+Ho6Gf3OTGUQY+leppEbcPPvs2w7Ra5+KnD0Irvwjv6SRPQWl/mbIpa63i5YecPAbl5o4fV6sQHL63aVny62psxfPOWDlui9aDq71W7hkE7jHnFpfdMeoC0oKF/CTt+D2PHtA9aEUzoPxMI46cpj1oQjs1XNtwzQ9WsRGcz9yJI8RoPSzVqrMowry7aou0WEtbMT+9vOILWj/7N7Lda28uS3rZ0b1aVRJX5hZ1WcrGdOJnWV6XZfkP65RHhg==
sidebar_class_name: "get api-method"
info_path: gen/api/agent-management-api
custom_edit_url: null
@@ -73,7 +73,7 @@ Retrieve jobs, optionally filtered by status.
diff --git a/docs/docs/gen/api/post-file.api.mdx b/docs/docs/gen/api/post-file.api.mdx
index 7ffd030f..bc46d5b9 100644
--- a/docs/docs/gen/api/post-file.api.mdx
+++ b/docs/docs/gen/api/post-file.api.mdx
@@ -5,7 +5,7 @@ description: "Upload a file to the Object Store."
sidebar_label: "Upload a file"
hide_title: true
hide_table_of_contents: true
-api: eJztWN9vGzcM/lcIPW3AxXFcO409DEM6LGsGFA2aBH1IgoB34vnUnKWrpIvjGve/D5Tu/CNx1j6sQAvkJfBFoviRH0lRXApJLrOq8spoMRGXVWlQAkKuSgJvwBcE79NPlHk498ZSTyTC49SJyZU4USXdvkONU5qR9rfHZ6e3LHdrKrLIJzpxk4jV16kUE3FmnGdBkQhHWW2VX4jJ1VK8IbRkj2tf8NF8zGRulSdx09wkokKLM/JkXdiscUZiInJjMz5IMfTPNdmFSISlz7WyJMUkx9JR8sjCjwVp8LamBNJFhc4FE6WakvOQFZTdAWoJWM5x4SBACDsYUQ8+kK+tdpAVqKckf+eDwNIUrSzJOTA5zAvyBdkglBntSXuQKs/JOsitmYUFelDOKz0FE3zbu9bsj6ygGYrJUvhFxfalxpSEvCQpx7r0rU0Nu4TtJOffGLlgkW0rL1rETGEdOO1tuSY4QLTwWHxWl15VaP1+buxsT6IPQJ5AinhFIirLvHpFjlcjIatdzlulp+Kx7xkV72Q3dT4FpXcGGT3grCr5MD1V+qGXGZ2LZoX5Nmr6msa3Zr7W5ApTlxJSAm8JPUmQNUuBpKo0ix5cC4vzaxFZd5Au+C+6PeV+g2vhaVaV6OlagCUtmc+58gX8bcDTg9/v1mP8TJn3HDPvIrkrBlkH26frGUd6/OpkxU2ThODfZRozg3xCqjSGWH+G9NZHPdE0m6RfRZpaBTdNE5ddZbSLNA76B09jibO1DSKS4OosI+fyuiwXzNNGDGFVlSoLqb7/ybHs9w6gFaqQnf8RNa7Awejw6wrO3x7vDUaHUKArtqJ05dJNHfQq7WfD4WB8lGcH2cFwjHmaD7Oj8fgwT8eD4eA10vCAhofDcTp+NcxwOB6Nxwfp66PRID0ajQIy9WXTcKU9Tck+ARZI4L2cLyEwt6Ac9AdDTo5Ylp4pIY/K4KpKbRrYVbYenHCl4Wr2ND8BS0soF1BQKQF1W8ViOvBmxxx1voy1NWbBCjAXoO+azfCLxTkYC11q/foIQci8ZxKkDZeWnbVfHwG+SYRXPhzG/MTL80ObUCLm17Dff5pSp/oeSyWhreFQ4aKr0f9TPpG1xn7do8ew8d0FfJAFX6AHk2W1tSS34/4EVUmS7xZL3iq6J3Aefe16HNGSPKrSfYNyKRX/xBJaGcDU1H4NYqdaWYdrTZOfG3sHXs3I1L4XLwf5Lcl0sTKSBbaUjPp9Zq2j9S/e9YTRHUXyUmPtC2PVF5KwB8dnp3BHC1jF1guxPwOxr3bcfsamSkrSsAen2tV5rjLFhbIiO1POhS73hd2fgd3xM71Nd5uFprxt6mLHHi7E9uaHS0fwR3hyxLbfGzD3ZEOzGO+WlyD40YNgtOs2DhvbXpIbiK6ZfKHzx6azScSMfGF4rFAZFzyPPD8Q+3k3YbD3cWZwsx43nDNvkZrNocMKauF91b3GQwsdNomk/XHSvcL++XgR+kelcxPEW6DH4em3notwMyASwUCizQe9fq/PPmLQM9Tr98/2/OWxs5briPzGQU00KLxOqxKVZqW1Lfmo6Kmr9i2YiIIdOLkSy2WKji5t2TT87zhV4ZmLVA7T8rm5yia2H3jEstMjd7TYGCbdY1nzHsFDlnu0iq3mCGoSURBKssEdUejPqH3vgs9dy+6apjRJJ3ScZVT5je1PqgurXoX22fvzC46+dtAzC4kVhgHh3GT9M4LE7XB+FL4BfLuEerGBYrmMOy7MHemmEZ1jPH+LhucF/wKdJ/xv
+api: eJztWN9v2zYQ/lcIPm2A7DiuncYehiEdljUDigZNgj4kRnASTxYbiVRJKo5q6H8fjpT8O2sfVqAF8mJIFo/33X13x+MtuUCbGFk6qRWf8psy1yAYsFTmyJxmLkP2Pv6EiWNXThvs84g7mFs+veXnMsf7d6BgjgUqd392eXFPcve6RAO0o+WziK/eLgSf8kttHQnyiFtMKiNdzae3S/4GwaA5q1xGW9M204WRDvmsmUW8BAMFOjTWL1ZQIJ/yVJuENpIE/XOFpuYRN/i5kgYFn6aQW4z4U09DKXuJFjhH1cMnZ6AXbFjyR8ilAEe76UI6LEpX8ybaccvHDBVzpsKIxXUJ1nq/CDlH61iSYfLAQAkG+QJqyzxuv4LM6LMP6CqjLEsyUHMUv9NGzOAcjMjRWqZTtsjQZWi8UKKVQ+WYkGmKxrLU6MJ/wCdpnVRzpj0h/TtFTkwyLIBMcXVJZsRa5wj0SWAKVe5aRzTkR3IOWvdGi5pEtq28bhET75UPhP6WP70DeAuPxIsqd7IE445SbYqeAOeB7EEKeHnES0PB4CR63wcWV6usM1LN+a7vCRWtJDd1PmVSHYxMfIKizGkzNZfqqZ9olRKbLeb7oOlrGt/qxVqTzXSVCxYjcwbBoWCiIikmsMx13Wd33MDijgfWLYtr+gXbk/Y3dscponJweMeZQSWIz4V0GftbM4dP7qj7HuJnTrynkDgbyF0xSDrIPlUVlB7hrZPlsybyGXPINGIGaIdYKvAJ8gzprY/6vGk2Sb8NNLUKZk0TPttSKxtoHA6O92OJUrwNIhTMVkmC1qZVntfE00YMQVnmMvH14eiTJdnvHUArVD47/yNqbAbD8cnXFVy9PesNxycsA5ttRenKpZs68FU8SEaj4eQ0TY6T49EE0jgdJaeTyUkaT4aj4WvA0TGOTkaTePJqlMBoMp5MjuPXp+NhfDoee2Tyy6bhUjmco9kD5kmgtZQvPjC3oBwPhiNKjlCWnikhO2VwVaU2DewqW5+dU6WharafnwxygyBqlmEuGKi2ioV0oMWWOOp8GWpryIIVYCpA3zWb2S8GFkwb1qXWrzsIfOY9kyBtuLTsrP26A3gWcSed34z4CSfuhzaheMiv0WCwn1IXyh9WrK3hrIS6q9H/Uz6hMdp83aNnbOO9C3gvy1wGjukkqYxBsR335yBzFHS2GHRG4iMy68BVth/OWwcyt9+gXAhJj5CzVoZBrCu3BnFQraj8sabQLbR5YE4WqCvXD4eD+JZkul4ZSQJbSsaDAbHW0foXrdpj9ECRvFFQuUwb+QUF67Gzywv2gDVbxdYLsT8Dsa8OnH7axFIIVKzHLpSt0lQmkgpliaaQ1vrW+IXdn4HdyTO9TXea+aa8bepCx+4PxPbkZzcW2R/+nhLafqeZfkTjm8VwtrwEwY8eBONDp7Ff2PaS1EB0zeQLnT82nU3EC3SZpllEqa33PNDQgR+l3VjCPIZBw2w9o7gi3gI1m5OKFdTMubK7jfsW2i/iUftw3t3C/vl47ftHqVLtxVugZ/7qtx6mUDPAI05Ags3H/UF/QD4i0AWo9f1ne2iz66zlOiK/cboTDPK30zIHqUhpZXLaKnjqtr0LRjwjB05v+XIZg8UbkzcN/R1GMTSoEdJCnG8MY57F9gOPWA565AHrjQnUI+QVreE0ZHkEI8lqiqAm4hmCQOPdEYT+DNp717TvWvbQNKWJOqGzJMHSbSzfqy6kehXal++vrin62kFP4RPLDwP8vtH6MYCE7XDeCV8Pvv0Eqt5AsVyGFdf6AVXT8M4xjt55Q/OCfwEf8g+R
sidebar_class_name: "post api-method"
info_path: gen/api/agent-management-api
custom_edit_url: null
@@ -68,7 +68,7 @@ Upload a file to the Object Store.
diff --git a/docs/docs/gen/api/undrain-agent.api.mdx b/docs/docs/gen/api/undrain-agent.api.mdx
index 2ba26015..1530c945 100644
--- a/docs/docs/gen/api/undrain-agent.api.mdx
+++ b/docs/docs/gen/api/undrain-agent.api.mdx
@@ -5,7 +5,7 @@ description: "Resume accepting jobs on a drained agent."
sidebar_label: "Undrain an agent"
hide_title: true
hide_table_of_contents: true
-api: eJztVl1r20oQ/SvLPN2CartfD1dvvtBACqUhdbgPwYSxdmxtIu2qs6O0rtB/v8xKdpw4Lb3QQgt58q52Ps8Z754OQkOM4oI/tZBD6y2j8/MNeYEMLMWCXaPHkMM5xbYmg0VBjTi/MddhFU3wBk3yImtQHSeQgeAmQn4JKdLVe/S4oVqX87PTq2R1tc8cYZlBpKJlJ1vILzv4h5CJ562UGiOZ55/ZCcGyX2bQIGNNQhyTtceaIIcyREnLDJyW26CUkAHTp9YxWciFW8ogFiXVCHkHsm3ULwo7v4H+YbuLkswupglrIyUN/RkJZgRqAloPU2yCjxQ16svZTH/ux0ow7JyM804cCllFqgheFO28A2yayhUJk+l1VMfuuNywuqZCyWlYERQ3pK0pRtzQI331hxhc7g2Xfa9Hr2cvjsu98NhKGdh9JWuem/nZqbmhrdmH+WlVE3Pg45ofUjE3B/sdF8nXSIliQlG0zAOe9AXrptJgJ+gqssoWk7CjWzJRUNo4GcgWdFX8geTWOl1iZUYfg6vQyl0Rj6a1LWlqT/I58I0RV1NoJaUugj0kynmhDfFR4sW+SXW4l+TNbKbkiZOU8q1anY9TCDtiXx0TexJ45awlb56bUx/b9doVTkezIa5djOnf+MTun8Du62/dMj6IWYfW/8zb5YnJX8jk399j0vnhcdX3NmVgG/SlVYToieHfn+E+g5qkDCqvmhAT8qpMcpgmNTHtdiKjn44CAVQO8e2gbw600UflcqDrUCHtyy9FGhgVju5XyQiycXESuEaBHN79u0iywPl1SO5j8cPU3Yk1ffghAy1kwOHFZDaZKW7aSI1pwEb1dTFqG/SDSHqIYXc3qP9PSA6tCX2RaVMpOH0GLVcaccBxFIiQQX4gAXdQLrMkDNWs61YY6YKrvtfPn1ri7QDwLbLDlWJw2YF1UdcW8jVWkb7Tx1/noyB6Zn5MLH6jnfEj+q2ijVWrO8jghraHurZf9hmUhJY4FToczxOKB45Ht4BK1P0Inn34uIAM8P7kPJiUFP7RqrpusFiEG/J9vy9SdK8V9v1/nZdG4w==
+api: eJztV01v20YQ/SuLOTXA6sNudCiBHFSgBlS0qOHI6MEQjBE5Itcmd5ndoSOV4H8PZknJsuUECZAeDPikXXFm5817A/JtC64mj2ycXWSQQGMzj8bOc7IMGjIKqTe1PIYErig0FSlMU6rZ2FzduXVQzipUMYsyhZI4Bg2MeYDkBuJJt3+jxZwqWc4vF7cx6vZQOcBKQ6C08YZ3kNy08DuhJz9vuJAzYnjy2RsmWHUrDTV6rIjJhxhtsSJIoHCB41KDEbg1cgEaPH1qjKcMEvYNadiOHNZmlLqMcrIj2rLHUQ+3hQcsTYYsx+3zdGXshzNd4fbD+WwGnYaQFlShhPOultDA3tgcNFTG/kU2F9hnGirc7nfns1n3nM5lQWqPWbmN4oJ6/hQ7NQgxBunXU6idDRQhnk+n8vP0rEjzPkkZa9ggUyZKpM6yqJm0gHVdmjRyPrkLktieNuPWd5SK+LUXhdj0ZSsKAXM67brrjjm+OQSuuk4evX8J7sJGog/t/0Sc5L3zL2nzjDB1tN+zH3MVF8jKpWnjfc8gbbGqSznsAk1Jmejjib2hB1KBkZswhigvoynDdxTPMiNLLNWQo3DtGn4E8WLZrCEpbYk/O3+v2FTkGo6lZZqP6hrLlJM/Kbw8NCkJT4rMplORiw3Hkn9I1NUwd7CX8uxUymuLDRfOm/8oUyM1v1yoe9qpw0S8CfsahP31VNgL59cmy8iqkVrY0Gw2JjXylqnJVyaE+OJ+U/c1qPv+ax8M61htXGN/5ofiTcn/UcnfvqWksb0PE2sWK/jMiSkTht4+sa9A4U5DRVw4ceK1C5F5MbEJTKIxnLR7w9RNBq8H4pz9Q2+Fj2z0R9Gyl+vYTB/gF8w1DFZW9usYBHpYXDhfIUMCf/67jA7P2I2L6QP4fuoefb18+EGDAOl5OBtPx1PhTRqpMA7YYNSvB5uKtve7zzlsHwf1x+4cfWtMW57UpZDTaWh8KSf2PA53CdCQHN0W9lSudLxDSFjbrjHQtS+7Tv7+1JDf9QQ/oDe4Fg5uWshMkHUGyQbLQN/o45erwRC9U9/n+7/SzvAn2p2wjWUjO9BwT7vjK1C36jQUhBn5CLR/PI8sHiWevAXktnEYwct/Pi5BAz6dnGeTEo9/EVXb9hFLd0+26w4gWfaCsOu+AGpsBvk=
sidebar_class_name: "post api-method"
info_path: gen/api/agent-management-api
custom_edit_url: null
@@ -68,7 +68,7 @@ Resume accepting jobs on a drained agent.
@@ -154,6 +154,97 @@ Resume accepting jobs on a drained agent.
+
+
+
+ Invalid hostname.
+
+
+
+
+
+
+
+
+
+
+ Schema
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/docs/sidebar/architecture/system-architecture.md b/docs/docs/sidebar/architecture/system-architecture.md
index d41b05e3..d4ee89c9 100644
--- a/docs/docs/sidebar/architecture/system-architecture.md
+++ b/docs/docs/sidebar/architecture/system-architecture.md
@@ -16,7 +16,7 @@ The system is organized into six layers, top to bottom:
| Layer | Package | Role |
| -------------------------- | --------------------------------------- | ------------------------------------------------------------------------ |
| **CLI** | `cmd/` | Cobra command tree (thin wiring) |
-| **SDK Client** | `osapi-sdk` (external) | OpenAPI-generated client used by CLI |
+| **SDK Client** | `pkg/sdk/osapi` | OpenAPI-generated client used by CLI |
| **REST API** | `internal/api/` | Echo server with JWT middleware |
| **Job Client** | `internal/job/client/` | Business logic for job CRUD and status |
| **NATS JetStream** | (external) | KV `job-queue`, Stream `JOBS`, KV `job-responses`, KV `agent-facts` |
@@ -24,7 +24,7 @@ The system is organized into six layers, top to bottom:
```mermaid
graph TD
- CLI["CLI (cmd/)"] --> SDK["SDK Client (osapi-sdk)"]
+ CLI["CLI (cmd/)"] --> SDK["SDK Client (pkg/sdk/osapi)"]
SDK --> API["REST API (internal/api/)"]
API --> JobClient["Job Client (internal/job/client/)"]
JobClient --> NATS["NATS JetStream"]
diff --git a/docs/docs/sidebar/features/_category_.json b/docs/docs/sidebar/features/_category_.json
deleted file mode 100644
index d4adb2ef..00000000
--- a/docs/docs/sidebar/features/_category_.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "label": "Features",
- "position": 3,
- "link": {
- "type": "generated-index",
- "title": "Features",
- "description": "OSAPI provides a comprehensive set of features for managing Linux systems. Each feature page describes what it does, how it works, relevant configuration, and links to CLI and API documentation."
- }
-}
diff --git a/docs/docs/sidebar/features/features.md b/docs/docs/sidebar/features/features.md
new file mode 100644
index 00000000..562fdae4
--- /dev/null
+++ b/docs/docs/sidebar/features/features.md
@@ -0,0 +1,26 @@
+---
+sidebar_position: 3
+---
+
+# Features
+
+OSAPI provides a comprehensive set of features for managing Linux systems.
+
+
+
+| | Feature | Description |
+| --- | ---------------------------------------------- | --------------------------------------------------------------------------------------------- |
+| 🖥️ | [Node Management](node-management.md) | Hostname, uptime, OS info, disk, memory, load |
+| 🌐 | [Network Management](network-management.md) | DNS read/update, ping |
+| ⚙️ | [Command Execution](command-execution.md) | Remote exec and shell across managed hosts |
+| 📁 | [File Management](file-management.md) | Upload, deploy, and template files with SHA-based idempotency |
+| 📊 | [System Facts](system-facts.md) | Agent-collected system facts -- architecture, kernel, FQDN, CPUs, network interfaces |
+| 🔄 | [Agent Lifecycle](agent-lifecycle.md) | Node conditions, graceful drain/cordon for maintenance |
+| ⚡ | [Job System](job-system.md) | NATS JetStream with KV-first architecture -- broadcast, load-balanced, and label-based routing |
+| 💚 | [Health Checks](health-checks.md) | Liveness, readiness, system status endpoints |
+| 📈 | [Metrics](metrics.md) | Prometheus `/metrics` endpoint |
+| 📋 | [Audit Logging](audit-logging.md) | Structured API audit trail with 30-day retention |
+| 🔐 | [Authentication & RBAC](authentication.md) | JWT with fine-grained `resource:verb` permissions |
+| 🔍 | [Distributed Tracing](distributed-tracing.md) | OpenTelemetry with trace context propagation |
+
+
diff --git a/docs/docs/sidebar/sdk/client/agent.md b/docs/docs/sidebar/sdk/client/agent.md
new file mode 100644
index 00000000..b4e6581a
--- /dev/null
+++ b/docs/docs/sidebar/sdk/client/agent.md
@@ -0,0 +1,34 @@
+---
+sidebar_position: 2
+---
+
+# AgentService
+
+Agent discovery and details.
+
+## Methods
+
+| Method | Description |
+| -------------------- | ----------------------------------- |
+| `List(ctx)` | Retrieve all active agents |
+| `Get(ctx, hostname)` | Get detailed agent info by hostname |
+
+## Usage
+
+```go
+// List all agents
+resp, err := client.Agent.List(ctx)
+
+// Get specific agent details
+resp, err := client.Agent.Get(ctx, "web-01")
+```
+
+## Example
+
+See
+[`examples/sdk/osapi/agent.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/osapi/agent.go)
+for a complete working example.
+
+## Permissions
+
+Requires `agent:read` permission.
diff --git a/docs/docs/sidebar/sdk/client/audit.md b/docs/docs/sidebar/sdk/client/audit.md
new file mode 100644
index 00000000..3fef0a70
--- /dev/null
+++ b/docs/docs/sidebar/sdk/client/audit.md
@@ -0,0 +1,38 @@
+---
+sidebar_position: 3
+---
+
+# AuditService
+
+Audit log operations.
+
+## Methods
+
+| Method | Description |
+| -------------------------- | -------------------------------- |
+| `List(ctx, limit, offset)` | Retrieve entries with pagination |
+| `Get(ctx, id)` | Retrieve a single entry by UUID |
+| `Export(ctx)` | Retrieve all entries for export |
+
+## Usage
+
+```go
+// List recent entries
+resp, err := client.Audit.List(ctx, 20, 0)
+
+// Get a specific entry
+resp, err := client.Audit.Get(ctx, "uuid-string")
+
+// Export all entries
+resp, err := client.Audit.Export(ctx)
+```
+
+## Example
+
+See
+[`examples/sdk/osapi/audit.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/osapi/audit.go)
+for a complete working example.
+
+## Permissions
+
+Requires `audit:read` permission.
diff --git a/docs/docs/sidebar/sdk/client/client.md b/docs/docs/sidebar/sdk/client/client.md
new file mode 100644
index 00000000..4abd7157
--- /dev/null
+++ b/docs/docs/sidebar/sdk/client/client.md
@@ -0,0 +1,51 @@
+---
+sidebar_position: 1
+---
+
+# Client
+
+The `osapi` package provides a typed Go client for the OSAPI REST API. Create a
+client with `New()` and use domain-specific services to interact with the API.
+
+## Quick Start
+
+```go
+import "github.com/retr0h/osapi/pkg/sdk/osapi"
+
+client := osapi.New("http://localhost:8080", "your-jwt-token")
+
+resp, err := client.Node.Hostname(ctx, "_any")
+```
+
+## Services
+
+| Service | Description |
+| --------------------- | ---------------------------------- |
+| [Agent](agent.md) | Agent discovery and details |
+| [Audit](audit.md) | Audit log operations |
+| [File](file.md) | File management (Object Store) |
+| [Health](health.md) | Health check operations |
+| [Job](job.md) | Async job queue operations |
+| [Metrics](metrics.md) | Prometheus metrics access |
+| [Node](node.md) | Node management, network, commands |
+
+## Client Options
+
+| Option | Description |
+| ------------------------------ | ------------------------------ |
+| `WithLogger(logger)` | Set custom `slog.Logger` |
+| `WithHTTPTransport(transport)` | Set custom `http.RoundTripper` |
+
+`WithLogger` defaults to `slog.Default()`. `WithHTTPTransport` sets the base
+transport for HTTP requests.
+
+## Targeting
+
+Most operations accept a `target` parameter:
+
+| Target | Behavior |
+| ----------- | ------------------------------------------- |
+| `_any` | Send to any available agent (load balanced) |
+| `_all` | Broadcast to every agent |
+| `hostname` | Send to a specific host |
+| `key:value` | Send to agents matching a label |
diff --git a/docs/docs/sidebar/sdk/client/file.md b/docs/docs/sidebar/sdk/client/file.md
new file mode 100644
index 00000000..825f3c0e
--- /dev/null
+++ b/docs/docs/sidebar/sdk/client/file.md
@@ -0,0 +1,141 @@
+---
+sidebar_position: 4
+---
+
+# FileService
+
+File management operations for the OSAPI Object Store. Upload, list, inspect,
+and delete files that can be deployed to agents via `Node.FileDeploy`.
+
+## Methods
+
+### Object Store
+
+| Method | Description |
+| ------------------------------- | ----------------------------------------------- |
+| `Upload(ctx, name, ct, r, ...)` | Upload file content to Object Store |
+| `Changed(ctx, name, r)` | Check if local content differs from stored file |
+| `List(ctx)` | List all stored files |
+| `Get(ctx, name)` | Get file metadata by name |
+| `Delete(ctx, name)` | Delete a file from Object Store |
+
+### Node File Operations
+
+File deploy and status methods live on `NodeService` because they target a
+specific host:
+
+| Method | Description |
+| ------------------------------- | ----------------------------------- |
+| `FileDeploy(ctx, opts)` | Deploy file to agent with SHA check |
+| `FileStatus(ctx, target, path)` | Check deployed file status |
+
+## FileDeployOpts
+
+| Field | Type | Required | Description |
+| ------------- | -------------- | -------- | ------------------------------------ |
+| `ObjectName` | string | Yes | Name of the file in Object Store |
+| `Path` | string | Yes | Destination path on the target host |
+| `ContentType` | string | Yes | `"raw"` or `"template"` |
+| `Mode` | string | No | File permission mode (e.g. `"0644"`) |
+| `Owner` | string | No | File owner user |
+| `Group` | string | No | File owner group |
+| `Vars` | map[string]any | No | Template variables for `"template"` |
+| `Target` | string | Yes | Host target (see Targeting below) |
+
+## Upload Options
+
+| Option | Description |
+| ------------- | ------------------------------------------------------- |
+| `WithForce()` | Bypass SDK-side and server-side SHA check; always write |
+
+## Usage
+
+```go
+// Upload a raw file.
+resp, err := client.File.Upload(
+ ctx, "nginx.conf", "raw", bytes.NewReader(data),
+)
+
+// Force upload — skip SHA-256 check, always write.
+resp, err := client.File.Upload(
+ ctx, "nginx.conf", "raw", bytes.NewReader(data),
+ osapi.WithForce(),
+)
+
+// Check if content differs without uploading.
+chk, err := client.File.Changed(
+ ctx, "nginx.conf", bytes.NewReader(data),
+)
+fmt.Println(chk.Data.Changed) // true if content differs
+
+// List all files.
+resp, err := client.File.List(ctx)
+
+// Get file metadata.
+resp, err := client.File.Get(ctx, "nginx.conf")
+
+// Delete a file.
+resp, err := client.File.Delete(ctx, "nginx.conf")
+
+// Deploy a raw file to a specific host.
+resp, err := client.Node.FileDeploy(ctx, osapi.FileDeployOpts{
+ ObjectName: "nginx.conf",
+ Path: "/etc/nginx/nginx.conf",
+ ContentType: "raw",
+ Mode: "0644",
+ Owner: "root",
+ Group: "root",
+ Target: "web-01",
+})
+
+// Deploy a template file with variables.
+resp, err := client.Node.FileDeploy(ctx, osapi.FileDeployOpts{
+ ObjectName: "app.conf.tmpl",
+ Path: "/etc/app/config.yaml",
+ ContentType: "template",
+ Vars: map[string]any{
+ "port": 8080,
+ "debug": false,
+ },
+ Target: "_all",
+})
+
+// Check file status on a host.
+resp, err := client.Node.FileStatus(
+ ctx, "web-01", "/etc/nginx/nginx.conf",
+)
+```
+
+## Targeting
+
+`FileDeploy` and `FileStatus` accept any valid target: `_any`, `_all`, a
+hostname, or a label selector (`key:value`).
+
+Object Store operations (`Upload`, `List`, `Get`, `Delete`) are server-side and
+do not use targeting.
+
+## Change Detection
+
+`Upload` computes a SHA-256 of the file content locally before uploading. If the
+hash matches the stored file, the upload is skipped and `Changed: false` is
+returned. Use `WithForce()` to bypass this check.
+
+`Changed` performs the same SHA-256 comparison without uploading. It returns
+`Changed: true` when the file does not exist or the content differs.
+
+## Idempotency
+
+`FileDeploy` compares the SHA-256 of the Object Store content against the
+deployed file. If the hashes match, the file is not rewritten and the response
+reports `Changed: false`.
+
+## Example
+
+See
+[`examples/sdk/osapi/file.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/osapi/file.go)
+for a complete working example.
+
+## Permissions
+
+Object Store operations require `file:read` (list, get) or `file:write` (upload,
+delete). Deploy requires `file:write`. Status requires `file:read`.
diff --git a/docs/docs/sidebar/sdk/client/health.md b/docs/docs/sidebar/sdk/client/health.md
new file mode 100644
index 00000000..bf0de274
--- /dev/null
+++ b/docs/docs/sidebar/sdk/client/health.md
@@ -0,0 +1,39 @@
+---
+sidebar_position: 5
+---
+
+# HealthService
+
+Health check operations.
+
+## Methods
+
+| Method | Description |
+| --------------- | ------------------------------------------ |
+| `Liveness(ctx)` | Check if API server process is alive |
+| `Ready(ctx)` | Check if server and dependencies are ready |
+| `Status(ctx)` | Detailed system status (components, NATS) |
+
+## Usage
+
+```go
+// Simple liveness check (unauthenticated)
+resp, err := client.Health.Liveness(ctx)
+
+// Readiness check (unauthenticated)
+resp, err := client.Health.Ready(ctx)
+
+// Detailed status (requires auth)
+resp, err := client.Health.Status(ctx)
+```
+
+## Example
+
+See
+[`examples/sdk/osapi/health.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/osapi/health.go)
+for a complete working example.
+
+## Permissions
+
+`Liveness` and `Ready` are unauthenticated. `Status` requires `health:read`
+permission.
diff --git a/docs/docs/sidebar/sdk/client/job.md b/docs/docs/sidebar/sdk/client/job.md
new file mode 100644
index 00000000..6301ffc0
--- /dev/null
+++ b/docs/docs/sidebar/sdk/client/job.md
@@ -0,0 +1,48 @@
+---
+sidebar_position: 6
+---
+
+# JobService
+
+Async job queue operations.
+
+## Methods
+
+| Method | Description |
+| -------------------------------- | ------------------------------- |
+| `Create(ctx, operation, target)` | Create a new job |
+| `Get(ctx, id)` | Retrieve a job by UUID |
+| `List(ctx, params)` | List jobs with optional filters |
+| `Delete(ctx, id)` | Delete a job by UUID |
+| `Retry(ctx, id, target)` | Retry a failed job |
+| `QueueStats(ctx)` | Retrieve queue statistics |
+
+## Usage
+
+```go
+// Create a job
+resp, err := client.Job.Create(ctx, map[string]any{
+ "type": "node.hostname.get",
+ "params": map[string]any{},
+}, "_any")
+
+// List completed jobs
+resp, err := client.Job.List(ctx, osapi.ListParams{
+ Status: "completed",
+ Limit: 20,
+})
+
+// Retry a failed job
+resp, err := client.Job.Retry(ctx, "uuid-string", "_any")
+```
+
+## Example
+
+See
+[`examples/sdk/osapi/job.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/osapi/job.go)
+for a complete working example.
+
+## Permissions
+
+Read operations require `job:read`. Write operations (create, delete, retry)
+require `job:write`.
diff --git a/docs/docs/sidebar/sdk/client/metrics.md b/docs/docs/sidebar/sdk/client/metrics.md
new file mode 100644
index 00000000..112d82c0
--- /dev/null
+++ b/docs/docs/sidebar/sdk/client/metrics.md
@@ -0,0 +1,30 @@
+---
+sidebar_position: 7
+---
+
+# MetricsService
+
+Prometheus metrics access.
+
+## Methods
+
+| Method | Description |
+| ---------- | --------------------------------- |
+| `Get(ctx)` | Fetch raw Prometheus metrics text |
+
+## Usage
+
+```go
+text, err := client.Metrics.Get(ctx)
+fmt.Print(text)
+```
+
+## Example
+
+See
+[`examples/sdk/osapi/metrics.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/osapi/metrics.go)
+for a complete working example.
+
+## Permissions
+
+Unauthenticated. The `/metrics` endpoint is open.
diff --git a/docs/docs/sidebar/sdk/client/node.md b/docs/docs/sidebar/sdk/client/node.md
new file mode 100644
index 00000000..ff9d03c1
--- /dev/null
+++ b/docs/docs/sidebar/sdk/client/node.md
@@ -0,0 +1,108 @@
+---
+sidebar_position: 8
+---
+
+# NodeService
+
+Node management, network configuration, and command execution. This is the
+largest service -- it combines node info, network, and command operations that
+all target a specific host.
+
+## Methods
+
+### Node Info
+
+| Method | Description |
+| ----------------------- | ----------------------------------------- |
+| `Status(ctx, target)` | Full node status (OS, disk, memory, load) |
+| `Hostname(ctx, target)` | Get system hostname |
+| `Disk(ctx, target)` | Get disk usage |
+| `Memory(ctx, target)` | Get memory statistics |
+| `Load(ctx, target)` | Get load averages |
+| `OS(ctx, target)` | Get operating system info |
+| `Uptime(ctx, target)` | Get uptime |
+
+### Network
+
+| Method | Description |
+| ------------------------------------------------ | ------------------ |
+| `GetDNS(ctx, target, iface)` | Get DNS config |
+| `UpdateDNS(ctx, target, iface, servers, search)` | Update DNS servers |
+| `Ping(ctx, target, address)` | Ping a host |
+
+### Command
+
+| Method | Description |
+| ----------------- | ------------------------------------------- |
+| `Exec(ctx, req)` | Execute a command directly (no shell) |
+| `Shell(ctx, req)` | Execute via `/bin/sh -c` (pipes, redirects) |
+
+### File
+
+| Method | Description |
+| ------------------------------- | ----------------------------------- |
+| `FileDeploy(ctx, opts)` | Deploy file to agent with SHA check |
+| `FileStatus(ctx, target, path)` | Check deployed file status |
+
+See [`FileService`](file.md) for Object Store operations (upload, list, get,
+delete) and `FileDeployOpts` details.
+
+## Usage
+
+```go
+// Get hostname
+resp, err := client.Node.Hostname(ctx, "_any")
+
+// Get disk usage from all hosts
+resp, err := client.Node.Disk(ctx, "_all")
+
+// Update DNS
+resp, err := client.Node.UpdateDNS(
+ ctx, "web-01", "eth0",
+ []string{"8.8.8.8", "8.8.4.4"},
+ nil,
+)
+
+// Execute a command
+resp, err := client.Node.Exec(ctx, osapi.ExecRequest{
+ Command: "apt",
+ Args: []string{"install", "-y", "nginx"},
+ Target: "_all",
+})
+
+// Execute a shell command
+resp, err := client.Node.Shell(ctx, osapi.ShellRequest{
+ Command: "ps aux | grep nginx",
+ Target: "_any",
+})
+
+// Deploy a file
+resp, err := client.Node.FileDeploy(ctx, osapi.FileDeployOpts{
+ ObjectName: "nginx.conf",
+ Path: "/etc/nginx/nginx.conf",
+ ContentType: "raw",
+ Mode: "0644",
+ Target: "web-01",
+})
+
+// Check file status
+resp, err := client.Node.FileStatus(
+ ctx, "web-01", "/etc/nginx/nginx.conf",
+)
+```
+
+## Examples
+
+See
+[`examples/sdk/osapi/node.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/osapi/node.go)
+for node info, and
+[`examples/sdk/osapi/network.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/osapi/network.go)
+and
+[`examples/sdk/osapi/command.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/osapi/command.go)
+for network and command examples.
+
+## Permissions
+
+Node info requires `node:read`. Network read requires `network:read`. DNS
+updates require `network:write`. Commands require `command:execute`. File deploy
+requires `file:write`. File status requires `file:read`.
diff --git a/docs/docs/sidebar/sdk/sdk.md b/docs/docs/sidebar/sdk/sdk.md
new file mode 100644
index 00000000..bbbed8fd
--- /dev/null
+++ b/docs/docs/sidebar/sdk/sdk.md
@@ -0,0 +1,11 @@
+---
+sidebar_position: 6
+---
+
+# SDK
+
+OSAPI provides a Go SDK for programmatic access to the REST API. The SDK
+includes a typed client library and a DAG-based orchestrator for composing
+multi-step operations.
+
+
diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts
index 47690288..4f253632 100644
--- a/docs/docusaurus.config.ts
+++ b/docs/docusaurus.config.ts
@@ -148,6 +148,58 @@ const config: Config = {
position: 'left',
docId: 'sidebar/usage/usage'
},
+ {
+ type: 'dropdown',
+ label: 'SDK',
+ position: 'left',
+ items: [
+ {
+ type: 'doc',
+ label: 'Overview',
+ docId: 'sidebar/sdk/sdk'
+ },
+ {
+ type: 'doc',
+ label: 'Client',
+ docId: 'sidebar/sdk/client/client'
+ },
+ {
+ type: 'doc',
+ label: 'Agent',
+ docId: 'sidebar/sdk/client/agent'
+ },
+ {
+ type: 'doc',
+ label: 'Audit',
+ docId: 'sidebar/sdk/client/audit'
+ },
+ {
+ type: 'doc',
+ label: 'File',
+ docId: 'sidebar/sdk/client/file'
+ },
+ {
+ type: 'doc',
+ label: 'Health',
+ docId: 'sidebar/sdk/client/health'
+ },
+ {
+ type: 'doc',
+ label: 'Job',
+ docId: 'sidebar/sdk/client/job'
+ },
+ {
+ type: 'doc',
+ label: 'Metrics',
+ docId: 'sidebar/sdk/client/metrics'
+ },
+ {
+ type: 'doc',
+ label: 'Node',
+ docId: 'sidebar/sdk/client/node'
+ }
+ ]
+ },
{
label: 'API',
position: 'left',
@@ -257,10 +309,8 @@ const config: Config = {
docsPluginId: 'classic',
config: {
osapi: {
- specPath: '../../osapi-sdk/pkg/osapi/gen/api.yaml',
+ specPath: '../internal/api/gen/api.yaml',
outputDir: 'docs/gen/api',
- downloadUrl:
- 'https://github.com/osapi-io/osapi-sdk/blob/main/pkg/osapi/gen/api.yaml',
sidebarOptions: {
groupPathsBy: 'tag',
categoryLinkSource: 'tag'
diff --git a/docs/plans/2026-03-07-sdk-monorepo-migration-design.md b/docs/plans/2026-03-07-sdk-monorepo-migration-design.md
new file mode 100644
index 00000000..b5d79a37
--- /dev/null
+++ b/docs/plans/2026-03-07-sdk-monorepo-migration-design.md
@@ -0,0 +1,181 @@
+# SDK Monorepo Migration
+
+**Date:** 2026-03-07 **Status:** Design **Author:** @retr0h
+
+## Problem
+
+The SDK living in a separate repo (`osapi-io/osapi-sdk`) creates friction:
+
+- OpenAPI specs must be synced via gilt overlay from osapi's `main` branch
+- Every API change requires a two-repo dance: merge osapi, run `just generate`
+ in the SDK, merge SDK, update `go.mod` in osapi
+- Per-example directories each with their own `go.mod` are a maintenance burden
+- The SDK has no external consumers — it's only used by the osapi CLI
+
+## Solution
+
+Move the SDK into the osapi repo as `pkg/sdk/`. Two incremental PRs.
+
+## Package Layout
+
+```
+pkg/sdk/
+├── osapi/ ← PR 1
+│ ├── gen/
+│ │ ├── cfg.yaml ← points to ../../../internal/api/gen/api.yaml
+│ │ ├── generate.go ← just oapi-codegen, no gilt
+│ │ └── client.gen.go
+│ ├── osapi.go
+│ ├── transport.go
+│ ├── errors.go
+│ ├── response.go
+│ ├── types.go
+│ ├── agent.go
+│ ├── agent_types.go
+│ ├── audit.go
+│ ├── audit_types.go
+│ ├── file.go
+│ ├── file_types.go
+│ ├── health.go
+│ ├── health_types.go
+│ ├── job.go
+│ ├── job_types.go
+│ ├── metrics.go
+│ ├── node.go
+│ ├── node_types.go
+│ └── *_test.go
+└── orchestrator/ ← PR 2
+ ├── plan.go
+ ├── task.go
+ ├── options.go
+ ├── result.go
+ ├── runner.go
+ └── *_test.go
+```
+
+Import paths change to:
+
+- `github.com/retr0h/osapi/pkg/sdk/osapi`
+- `github.com/retr0h/osapi/pkg/sdk/orchestrator`
+
+## Spec Generation
+
+No more gilt. The `cfg.yaml` in `pkg/sdk/osapi/gen/` references the server's
+combined spec directly:
+
+```yaml
+# cfg.yaml
+input: ../../../internal/api/gen/api.yaml
+```
+
+Single source of truth. Specs can never drift. Regenerate with
+`go generate ./pkg/sdk/...`.
+
+## Examples
+
+Flatten from per-directory modules to individual files in two directories:
+
+```
+examples/sdk/
+├── osapi/
+│ ├── go.mod ← replace ../../../pkg/sdk
+│ ├── go.sum
+│ ├── health.go ← go run health.go
+│ ├── node.go
+│ ├── agent.go
+│ ├── audit.go
+│ ├── command.go
+│ ├── file.go
+│ ├── job.go
+│ ├── metrics.go
+│ └── network.go
+└── orchestrator/
+ ├── go.mod ← replace ../../../pkg/sdk
+ ├── go.sum
+ ├── basic.go
+ ├── parallel.go
+ ├── guards.go
+ ├── hooks.go
+ ├── retry.go
+ ├── broadcast.go
+ ├── error_strategy.go
+ ├── file_deploy.go
+ ├── only_if_changed.go
+ ├── only_if_failed.go
+ ├── result_decode.go
+ ├── task_func.go
+ └── task_func_results.go
+```
+
+All files are `package main`. Run with `go run health.go`.
+
+## Documentation
+
+### Docusaurus SDK Sidebar
+
+New top-level sidebar section:
+
+```
+docs/docs/sidebar/sdk/
+├── sdk.md ← Overview with DocCardList
+├── client/
+│ ├── client.md ← Client overview, New(), options, transport
+│ ├── agent.md
+│ ├── audit.md
+│ ├── file.md
+│ ├── health.md
+│ ├── job.md
+│ ├── metrics.md
+│ └── node.md
+└── orchestrator/
+ ├── orchestrator.md ← Overview, Plan/Task/Run
+ ├── operations.md ← Built-in operations reference
+ ├── hooks.md ← Hooks and error strategies
+ └── examples.md ← Example walkthroughs
+```
+
+Content migrated from the osapi-sdk `docs/osapi/` and `docs/orchestration/`
+directories. Landing page uses `` cards.
+
+### README and CLAUDE.md Updates
+
+- **README.md**: Add SDK link in the docs/features section. Remove sibling repo
+ references.
+- **CLAUDE.md**: Update SDK references to reflect `pkg/sdk/` location. Simplify
+ "Adding a New API Domain" Step 5 — no gilt, just `go generate ./pkg/sdk/...`.
+ Remove sibling repo references but keep SDK documentation (now pointing to
+ in-repo paths).
+- **docusaurus.config.ts**: Add "SDK" to the navbar Features dropdown.
+
+## Cleanup
+
+### PR 1 (SDK client)
+
+- Copy `osapi-sdk/pkg/osapi/` → `pkg/sdk/osapi/`
+- Update `pkg/sdk/osapi/gen/cfg.yaml` to reference `internal/api/gen/api.yaml`
+- Remove `generate.go` gilt step (oapi-codegen only)
+- Flatten `osapi-sdk/examples/osapi/` → `examples/sdk/osapi/`
+- Update all `cmd/*.go` imports: `github.com/osapi-io/osapi-sdk/pkg/osapi` →
+ `github.com/retr0h/osapi/pkg/sdk/osapi`
+- Remove `github.com/osapi-io/osapi-sdk` from `go.mod`
+- Create Docusaurus client pages
+- Update README.md, CLAUDE.md
+
+### PR 2 (Orchestrator)
+
+- Copy `osapi-sdk/pkg/orchestrator/` → `pkg/sdk/orchestrator/`
+- Update orchestrator imports to use new SDK client path
+- Flatten `osapi-sdk/examples/orchestration/` → `examples/sdk/orchestrator/`
+- Create Docusaurus orchestrator pages
+
+### Post-merge
+
+- User archives `osapi-io/osapi-sdk` repo on GitHub
+
+## Scalability Note: `kv.Keys()`
+
+Not related to this migration but documented here for context — the SDK's
+`QueueStats()` and `List()` methods rely on the server's `kv.Keys()` call. See
+the [Job Architecture](../docs/sidebar/architecture/job-architecture.md)
+performance section for the known scalability constraint and mitigation
+approaches.
diff --git a/docs/plans/2026-03-07-sdk-monorepo-migration.md b/docs/plans/2026-03-07-sdk-monorepo-migration.md
new file mode 100644
index 00000000..982e32ea
--- /dev/null
+++ b/docs/plans/2026-03-07-sdk-monorepo-migration.md
@@ -0,0 +1,522 @@
+# SDK Monorepo Migration (PR 1: Client) Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to
+> implement this plan task-by-task.
+
+**Goal:** Move the SDK client library from `osapi-io/osapi-sdk` into this repo
+as `pkg/sdk/osapi/`, flatten examples, add Docusaurus SDK docs, and update all
+references.
+
+**Architecture:** Copy `osapi-sdk/pkg/osapi/` into `pkg/sdk/osapi/`, rewrite the
+codegen to read the server's combined spec directly (no gilt), update all 18 Go
+import paths, flatten 9 example directories into individual files, create
+Docusaurus SDK sidebar pages, and clean up CLAUDE.md/README.md references.
+
+**Tech Stack:** Go, oapi-codegen, Docusaurus, Cobra CLI
+
+**Design doc:** `docs/plans/2026-03-07-sdk-monorepo-migration-design.md`
+
+---
+
+### Task 1: Copy SDK client package
+
+**Files:**
+
+- Create: `pkg/sdk/osapi/` (all `.go` files from `osapi-sdk/pkg/osapi/`)
+- Create: `pkg/sdk/osapi/gen/` (cfg.yaml, generate.go, client.gen.go)
+
+**Step 1: Copy source files**
+
+```bash
+mkdir -p pkg/sdk/osapi/gen
+cp ../osapi-sdk/pkg/osapi/*.go pkg/sdk/osapi/
+cp ../osapi-sdk/pkg/osapi/gen/client.gen.go pkg/sdk/osapi/gen/
+```
+
+**Step 2: Create new cfg.yaml**
+
+Create `pkg/sdk/osapi/gen/cfg.yaml`:
+
+```yaml
+---
+package: gen
+output: client.gen.go
+generate:
+ models: true
+ client: true
+output-options:
+ skip-prune: true
+```
+
+**Step 3: Create new generate.go**
+
+Create `pkg/sdk/osapi/gen/generate.go`:
+
+```go
+// Package gen contains generated code for the OSAPI REST API client.
+package gen
+
+//go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg.yaml ../../../internal/api/gen/api.yaml
+```
+
+No gilt — oapi-codegen reads the server's combined spec directly.
+
+**Step 4: Update package import paths in all copied files**
+
+In every `.go` file under `pkg/sdk/osapi/` (non-test, non-gen), replace:
+
+```
+"github.com/osapi-io/osapi-sdk/pkg/osapi/gen"
+```
+
+with:
+
+```
+"github.com/retr0h/osapi/pkg/sdk/osapi/gen"
+```
+
+In every `_test.go` file under `pkg/sdk/osapi/`, replace:
+
+```
+"github.com/osapi-io/osapi-sdk/pkg/osapi/gen"
+```
+
+with:
+
+```
+"github.com/retr0h/osapi/pkg/sdk/osapi/gen"
+```
+
+And for public test files, replace:
+
+```
+"github.com/osapi-io/osapi-sdk/pkg/osapi"
+```
+
+with:
+
+```
+"github.com/retr0h/osapi/pkg/sdk/osapi"
+```
+
+**Step 5: Regenerate client to verify**
+
+```bash
+cd pkg/sdk/osapi/gen && go generate ./...
+```
+
+Verify `client.gen.go` is regenerated without errors.
+
+**Step 6: Commit**
+
+```bash
+git add pkg/sdk/
+git commit -m "feat(sdk): copy client library into pkg/sdk/osapi"
+```
+
+---
+
+### Task 2: Update Go imports and remove external SDK dependency
+
+**Files:**
+
+- Modify: `go.mod` (remove `github.com/osapi-io/osapi-sdk` require)
+- Modify: 18 Go files (update import paths)
+
+**Step 1: Update all Go imports**
+
+In every file listed below, replace `"github.com/osapi-io/osapi-sdk/pkg/osapi"`
+with `"github.com/retr0h/osapi/pkg/sdk/osapi"`:
+
+- `cmd/client.go`
+- `cmd/client_agent_get.go`
+- `cmd/client_audit_export.go`
+- `cmd/client_file_upload.go` (aliased import: `osapi "..."`)
+- `cmd/client_health_status.go`
+- `cmd/client_job_list.go`
+- `cmd/client_job_run.go`
+- `cmd/client_node_command_exec.go`
+- `cmd/client_node_command_shell.go`
+- `cmd/client_node_file_deploy.go`
+- `cmd/client_node_status_get.go`
+- `internal/audit/export/types.go`
+- `internal/audit/export/file.go`
+- `internal/audit/export/file_test.go`
+- `internal/audit/export/export_public_test.go`
+- `internal/audit/export/file_public_test.go`
+- `internal/cli/ui.go`
+- `internal/cli/ui_public_test.go`
+
+**Step 2: Remove external SDK from go.mod**
+
+Remove the `github.com/osapi-io/osapi-sdk` line from the `require` block in
+`go.mod`. Then run:
+
+```bash
+go mod tidy
+```
+
+This will remove the SDK from `go.sum` as well.
+
+**Step 3: Build and test**
+
+```bash
+go build ./...
+go test ./... -count=1 -timeout 120s
+```
+
+**Step 4: Commit**
+
+```bash
+git add -A
+git commit -m "refactor(sdk): update imports to pkg/sdk/osapi"
+```
+
+---
+
+### Task 3: Flatten SDK client examples
+
+**Files:**
+
+- Create: `examples/sdk/osapi/go.mod`
+- Create: `examples/sdk/osapi/health.go` (and 8 more)
+
+**Step 1: Create examples directory and go.mod**
+
+```bash
+mkdir -p examples/sdk/osapi
+```
+
+Create `examples/sdk/osapi/go.mod`:
+
+```
+module github.com/retr0h/osapi/examples/sdk/osapi
+
+go 1.25.0
+
+replace github.com/retr0h/osapi => ../../../
+
+require github.com/retr0h/osapi v0.0.0
+```
+
+Then run:
+
+```bash
+cd examples/sdk/osapi && go mod tidy
+```
+
+**Step 2: Create flattened example files**
+
+Copy each example's `main.go` into a single file, updating the import path. Each
+file is `package main` and self-contained.
+
+From `../osapi-sdk/examples/osapi/`:
+
+| Source directory | Target file |
+| ----------------- | ------------------------------- |
+| `health/main.go` | `examples/sdk/osapi/health.go` |
+| `node/main.go` | `examples/sdk/osapi/node.go` |
+| `agent/main.go` | `examples/sdk/osapi/agent.go` |
+| `audit/main.go` | `examples/sdk/osapi/audit.go` |
+| `command/main.go` | `examples/sdk/osapi/command.go` |
+| `file/main.go` | `examples/sdk/osapi/file.go` |
+| `job/main.go` | `examples/sdk/osapi/job.go` |
+| `metrics/main.go` | `examples/sdk/osapi/metrics.go` |
+| `network/main.go` | `examples/sdk/osapi/network.go` |
+
+In each file, replace:
+
+```
+"github.com/osapi-io/osapi-sdk/pkg/osapi"
+```
+
+with:
+
+```
+"github.com/retr0h/osapi/pkg/sdk/osapi"
+```
+
+**Step 3: Verify examples compile**
+
+```bash
+cd examples/sdk/osapi && go build ./...
+```
+
+Note: `go build ./...` on `package main` files in the same directory will verify
+they all compile. They won't run without a live server, but compilation proves
+imports are correct.
+
+**Step 4: Commit**
+
+```bash
+git add examples/sdk/
+git commit -m "feat(sdk): add flattened client examples"
+```
+
+---
+
+### Task 4: Create Docusaurus SDK client pages
+
+**Files:**
+
+- Create: `docs/docs/sidebar/sdk/sdk.md`
+- Create: `docs/docs/sidebar/sdk/client/client.md`
+- Create: `docs/docs/sidebar/sdk/client/agent.md`
+- Create: `docs/docs/sidebar/sdk/client/audit.md`
+- Create: `docs/docs/sidebar/sdk/client/file.md`
+- Create: `docs/docs/sidebar/sdk/client/health.md`
+- Create: `docs/docs/sidebar/sdk/client/job.md`
+- Create: `docs/docs/sidebar/sdk/client/metrics.md`
+- Create: `docs/docs/sidebar/sdk/client/node.md`
+- Modify: `docs/docusaurus.config.ts`
+
+**Step 1: Create SDK landing page**
+
+Create `docs/docs/sidebar/sdk/sdk.md`:
+
+```markdown
+---
+sidebar_position: 6
+---
+
+# SDK
+
+OSAPI provides a Go SDK for programmatic access to the REST API. The SDK
+includes a typed client library and a DAG-based orchestrator for composing
+multi-step operations.
+
+
+```
+
+**Step 2: Create client overview page**
+
+Create `docs/docs/sidebar/sdk/client/client.md`. Migrate content from
+`osapi-sdk/docs/osapi/README.md`: services table, client options, targeting
+table. Adapt to Docusaurus format with `` for per-service pages.
+
+**Step 3: Create per-service pages**
+
+Create one page per service (`agent.md`, `audit.md`, `file.md`, `health.md`,
+`job.md`, `metrics.md`, `node.md`). Migrate content from
+`osapi-sdk/docs/osapi/{service}.md`. Each page covers the service methods,
+parameters, return types, and a usage example.
+
+**Step 4: Update docusaurus.config.ts**
+
+Add "SDK" to the Features navbar dropdown:
+
+```typescript
+{
+ label: 'SDK',
+ to: 'sidebar/sdk/sdk',
+},
+```
+
+Update the `specPath` for the API docs plugin from
+`../../osapi-sdk/pkg/osapi/gen/api.yaml` to `../internal/api/gen/api.yaml`.
+
+**Step 5: Update the API docs specPath**
+
+In `docs/docusaurus.config.ts`, change:
+
+```typescript
+specPath: '../../osapi-sdk/pkg/osapi/gen/api.yaml',
+```
+
+to:
+
+```typescript
+specPath: '../internal/api/gen/api.yaml',
+```
+
+And remove the GitHub download URL reference to the SDK repo.
+
+**Step 6: Verify docs build**
+
+```bash
+cd docs && bun run build
+```
+
+**Step 7: Commit**
+
+```bash
+git add docs/
+git commit -m "docs(sdk): add client library pages to Docusaurus"
+```
+
+---
+
+### Task 5: Update CLAUDE.md
+
+**Files:**
+
+- Modify: `CLAUDE.md`
+
+**Step 1: Update architecture section**
+
+Change line ~41 from:
+
+```
+- **`osapi-sdk`** - External SDK for programmatic REST API access (sibling repo, linked via `replace` in `go.mod`)
+```
+
+to:
+
+```
+- **`pkg/sdk/`** - Go SDK for programmatic REST API access (`osapi/` client library, `orchestrator/` DAG runner)
+```
+
+**Step 2: Rewrite "Update SDK" Step 5**
+
+Replace the entire Step 5 section (lines ~174-196) with:
+
+```markdown
+### Step 5: Update SDK
+
+The SDK client library lives in `pkg/sdk/osapi/`. Its generated HTTP client uses
+the same combined OpenAPI spec as the server (`internal/api/gen/api.yaml`).
+
+**When modifying existing API specs:**
+
+1. Make changes to `internal/api/{domain}/gen/api.yaml` in this repo
+2. Run `just generate` to regenerate server code (this also regenerates the
+ combined spec via `redocly join`)
+3. Run `go generate ./pkg/sdk/osapi/gen/...` to regenerate the SDK client
+4. Update the SDK service wrappers in `pkg/sdk/osapi/{domain}.go` if new
+ response codes were added
+5. Update CLI switch blocks in `cmd/` if new response codes were added
+
+**When adding a new API domain:**
+
+1. Add a service wrapper in `pkg/sdk/osapi/{domain}.go`
+2. Run `go generate ./pkg/sdk/osapi/gen/...` to pick up the new domain's spec
+ from the combined `api.yaml`
+```
+
+**Step 3: Remove sibling repo references**
+
+Remove any remaining references to `osapi-sdk` as a "sibling repo" or "external"
+dependency. Keep documentation about the SDK but update paths to `pkg/sdk/`.
+
+**Step 4: Commit**
+
+```bash
+git add CLAUDE.md
+git commit -m "docs: update CLAUDE.md for in-repo SDK"
+```
+
+---
+
+### Task 6: Update README.md and system-architecture.md
+
+**Files:**
+
+- Modify: `README.md`
+- Modify: `docs/docs/sidebar/architecture/system-architecture.md`
+
+**Step 1: Update README.md**
+
+Replace the "Sister Projects" section. Remove `osapi-sdk` from the sister
+projects table (it's now in-repo). Add an SDK link in the Documentation section:
+
+```markdown
+## 📖 Documentation
+
+- [Getting Started](https://osapi-io.github.io/osapi/)
+- [Features](https://osapi-io.github.io/osapi/sidebar/features/)
+- [SDK](https://osapi-io.github.io/osapi/sidebar/sdk/sdk)
+- [CLI Reference](https://osapi-io.github.io/osapi/sidebar/usage/)
+- [Architecture](https://osapi-io.github.io/osapi/sidebar/architecture/)
+```
+
+If `osapi-orchestrator` is still a separate repo, keep it in sister projects but
+remove `osapi-sdk`.
+
+**Step 2: Update system-architecture.md**
+
+Change line ~19 from:
+
+```
+| **SDK Client** | `osapi-sdk` (external) | OpenAPI-generated client used by CLI |
+```
+
+to:
+
+```
+| **SDK Client** | `pkg/sdk/osapi` | OpenAPI-generated client used by CLI |
+```
+
+Update the mermaid diagram reference from `SDK["SDK Client (osapi-sdk)"]` to
+`SDK["SDK Client (pkg/sdk/osapi)"]`.
+
+**Step 3: Commit**
+
+```bash
+git add README.md docs/docs/sidebar/architecture/system-architecture.md
+git commit -m "docs: update README and architecture for in-repo SDK"
+```
+
+---
+
+### Task 7: Final verification
+
+**Step 1: Full build**
+
+```bash
+go build ./...
+```
+
+**Step 2: Full test suite**
+
+```bash
+go test ./... -count=1 -timeout 120s
+```
+
+**Step 3: Lint**
+
+```bash
+just go::vet
+```
+
+**Step 4: Regenerate to verify codegen pipeline**
+
+```bash
+go generate ./pkg/sdk/osapi/gen/...
+go build ./...
+```
+
+**Step 5: Verify examples compile**
+
+```bash
+cd examples/sdk/osapi && go build ./...
+```
+
+**Step 6: Verify docs build**
+
+```bash
+cd docs && bun run build
+```
+
+---
+
+## Files Summary
+
+| Action | Path |
+| ------ | ------------------------------------------------------- |
+| Create | `pkg/sdk/osapi/*.go` (all source + test files) |
+| Create | `pkg/sdk/osapi/gen/cfg.yaml` |
+| Create | `pkg/sdk/osapi/gen/generate.go` |
+| Create | `pkg/sdk/osapi/gen/client.gen.go` |
+| Create | `examples/sdk/osapi/go.mod` |
+| Create | `examples/sdk/osapi/*.go` (9 example files) |
+| Create | `docs/docs/sidebar/sdk/sdk.md` |
+| Create | `docs/docs/sidebar/sdk/client/client.md` |
+| Create | `docs/docs/sidebar/sdk/client/{service}.md` (7 files) |
+| Modify | `cmd/*.go` (11 files — import path) |
+| Modify | `internal/audit/export/*.go` (5 files — import path) |
+| Modify | `internal/cli/ui.go`, `ui_public_test.go` (import path) |
+| Modify | `go.mod` (remove external SDK) |
+| Modify | `CLAUDE.md` |
+| Modify | `README.md` |
+| Modify | `docs/docs/sidebar/architecture/system-architecture.md` |
+| Modify | `docs/docusaurus.config.ts` |
diff --git a/examples/sdk/osapi/agent.go b/examples/sdk/osapi/agent.go
new file mode 100644
index 00000000..0022f73d
--- /dev/null
+++ b/examples/sdk/osapi/agent.go
@@ -0,0 +1,116 @@
+// 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.
+
+//go:build ignore
+
+// 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 agent.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/retr0h/osapi/pkg/sdk/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/sdk/osapi/audit.go b/examples/sdk/osapi/audit.go
new file mode 100644
index 00000000..4b41347e
--- /dev/null
+++ b/examples/sdk/osapi/audit.go
@@ -0,0 +1,82 @@
+// 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.
+
+//go:build ignore
+
+// Package main demonstrates the AuditService: listing audit entries,
+// retrieving a specific entry, and exporting all entries.
+//
+// Run with: OSAPI_TOKEN="" go run audit.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/retr0h/osapi/pkg/sdk/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/sdk/osapi/command.go b/examples/sdk/osapi/command.go
new file mode 100644
index 00000000..7a2df56c
--- /dev/null
+++ b/examples/sdk/osapi/command.go
@@ -0,0 +1,82 @@
+// 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.
+
+//go:build ignore
+
+// Package main demonstrates command execution: direct exec and
+// shell-interpreted commands.
+//
+// Run with: OSAPI_TOKEN="" go run command.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/retr0h/osapi/pkg/sdk/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/sdk/osapi/file.go b/examples/sdk/osapi/file.go
new file mode 100644
index 00000000..67dba838
--- /dev/null
+++ b/examples/sdk/osapi/file.go
@@ -0,0 +1,144 @@
+// 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.
+
+//go:build ignore
+
+// Package main demonstrates file management: upload, check for changes,
+// force upload, list, get metadata, deploy to an agent, check status,
+// and delete.
+//
+// Run with: OSAPI_TOKEN="" go run file.go
+package main
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/retr0h/osapi/pkg/sdk/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()
+
+ // Upload a raw file to the Object Store.
+ content := []byte("listen_address = 0.0.0.0:8080\nworkers = 4\n")
+ upload, err := client.File.Upload(
+ ctx,
+ "app.conf",
+ "raw",
+ bytes.NewReader(content),
+ )
+ if err != nil {
+ log.Fatalf("upload: %v", err)
+ }
+
+ fmt.Printf("Uploaded: name=%s sha256=%s size=%d changed=%v\n",
+ upload.Data.Name, upload.Data.SHA256, upload.Data.Size, upload.Data.Changed)
+
+ // Check if the file has changed without uploading.
+ chk, err := client.File.Changed(ctx, "app.conf", bytes.NewReader(content))
+ if err != nil {
+ log.Fatalf("changed: %v", err)
+ }
+
+ fmt.Printf("Changed: name=%s changed=%v\n", chk.Data.Name, chk.Data.Changed)
+
+ // Force upload bypasses both SDK-side and server-side checks.
+ force, err := client.File.Upload(
+ ctx,
+ "app.conf",
+ "raw",
+ bytes.NewReader(content),
+ osapi.WithForce(),
+ )
+ if err != nil {
+ log.Fatalf("force upload: %v", err)
+ }
+
+ fmt.Printf("Force upload: name=%s changed=%v\n",
+ force.Data.Name, force.Data.Changed)
+
+ // List all stored files.
+ list, err := client.File.List(ctx)
+ if err != nil {
+ log.Fatalf("list: %v", err)
+ }
+
+ fmt.Printf("\nStored files (%d):\n", list.Data.Total)
+ for _, f := range list.Data.Files {
+ fmt.Printf(" %s size=%d\n", f.Name, f.Size)
+ }
+
+ // Get metadata for a specific file.
+ meta, err := client.File.Get(ctx, "app.conf")
+ if err != nil {
+ log.Fatalf("get: %v", err)
+ }
+
+ fmt.Printf("\nMetadata: name=%s sha256=%s size=%d\n",
+ meta.Data.Name, meta.Data.SHA256, meta.Data.Size)
+
+ // Deploy the file to an agent.
+ deploy, err := client.Node.FileDeploy(ctx, osapi.FileDeployOpts{
+ ObjectName: "app.conf",
+ Path: "/tmp/app.conf",
+ ContentType: "raw",
+ Mode: "0644",
+ Target: "_any",
+ })
+ if err != nil {
+ log.Fatalf("deploy: %v", err)
+ }
+
+ fmt.Printf("\nDeploy: job=%s host=%s changed=%v\n",
+ deploy.Data.JobID, deploy.Data.Hostname, deploy.Data.Changed)
+
+ // Check file status on the agent.
+ status, err := client.Node.FileStatus(ctx, "_any", "/tmp/app.conf")
+ if err != nil {
+ log.Fatalf("status: %v", err)
+ }
+
+ fmt.Printf("Status: path=%s status=%s\n",
+ status.Data.Path, status.Data.Status)
+
+ // Clean up — delete the file from the Object Store.
+ del, err := client.File.Delete(ctx, "app.conf")
+ if err != nil {
+ log.Fatalf("delete: %v", err)
+ }
+
+ fmt.Printf("\nDeleted: name=%s deleted=%v\n",
+ del.Data.Name, del.Data.Deleted)
+}
diff --git a/examples/sdk/osapi/go.mod b/examples/sdk/osapi/go.mod
new file mode 100644
index 00000000..ade9f14b
--- /dev/null
+++ b/examples/sdk/osapi/go.mod
@@ -0,0 +1,20 @@
+module github.com/retr0h/osapi/examples/sdk/osapi
+
+go 1.25.0
+
+replace github.com/retr0h/osapi => ../../../
+
+require github.com/retr0h/osapi v0.0.0
+
+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.41.0 // indirect
+ go.opentelemetry.io/otel/metric v1.41.0 // indirect
+ go.opentelemetry.io/otel/trace v1.41.0 // indirect
+)
diff --git a/examples/sdk/osapi/go.sum b/examples/sdk/osapi/go.sum
new file mode 100644
index 00000000..11fca53b
--- /dev/null
+++ b/examples/sdk/osapi/go.sum
@@ -0,0 +1,29 @@
+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/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/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/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=
+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.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
+go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
+go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
+go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
+go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
+go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
diff --git a/examples/sdk/osapi/health.go b/examples/sdk/osapi/health.go
new file mode 100644
index 00000000..c43c0a64
--- /dev/null
+++ b/examples/sdk/osapi/health.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.
+
+//go:build ignore
+
+// Package main demonstrates the HealthService: liveness, readiness,
+// and detailed system status checks.
+//
+// Run with: OSAPI_TOKEN="" go run health.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/retr0h/osapi/pkg/sdk/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/sdk/osapi/job.go b/examples/sdk/osapi/job.go
new file mode 100644
index 00000000..517821df
--- /dev/null
+++ b/examples/sdk/osapi/job.go
@@ -0,0 +1,94 @@
+// 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.
+
+//go:build ignore
+
+// Package main demonstrates the JobService: creating a job, polling
+// for its result, listing jobs, and checking queue statistics.
+//
+// Run with: OSAPI_TOKEN="" go run job.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+ "time"
+
+ "github.com/retr0h/osapi/pkg/sdk/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/sdk/osapi/metrics.go b/examples/sdk/osapi/metrics.go
new file mode 100644
index 00000000..24d3bea8
--- /dev/null
+++ b/examples/sdk/osapi/metrics.go
@@ -0,0 +1,58 @@
+// 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.
+
+//go:build ignore
+
+// Package main demonstrates the MetricsService: fetching raw
+// Prometheus metrics text from the /metrics endpoint.
+//
+// Run with: OSAPI_TOKEN="" go run metrics.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/retr0h/osapi/pkg/sdk/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/sdk/osapi/network.go b/examples/sdk/osapi/network.go
new file mode 100644
index 00000000..e46ce0eb
--- /dev/null
+++ b/examples/sdk/osapi/network.go
@@ -0,0 +1,81 @@
+// 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.
+
+//go:build ignore
+
+// Package main demonstrates network operations: reading DNS config
+// and running a ping check.
+//
+// Run with: OSAPI_TOKEN="" go run network.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/retr0h/osapi/pkg/sdk/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/sdk/osapi/node.go b/examples/sdk/osapi/node.go
new file mode 100644
index 00000000..2fd58012
--- /dev/null
+++ b/examples/sdk/osapi/node.go
@@ -0,0 +1,144 @@
+// 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.
+
+//go:build ignore
+
+// 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 node.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/retr0h/osapi/pkg/sdk/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/go.mod b/go.mod
index 73ebf8ab..0f708c78 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,6 @@ require (
github.com/oapi-codegen/runtime v1.2.0
github.com/osapi-io/nats-client v0.0.0-20260306210421-d68b2a0f287b
github.com/osapi-io/nats-server v0.0.0-20260216201410-1f33dfc63848
- github.com/osapi-io/osapi-sdk v0.0.0-20260307192743-857786ce1c9e
github.com/prometheus-community/pro-bing v0.8.0
github.com/prometheus/client_golang v1.23.2
github.com/samber/slog-echo v1.21.0
@@ -144,7 +143,7 @@ require (
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
github.com/golangci/go-printf-func-name v0.1.1 // indirect
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
- github.com/golangci/golangci-lint/v2 v2.11.1 // indirect
+ github.com/golangci/golangci-lint/v2 v2.11.2 // indirect
github.com/golangci/golines v0.15.0 // indirect
github.com/golangci/misspell v0.8.0 // indirect
github.com/golangci/plugin-module-register v0.1.2 // indirect
diff --git a/go.sum b/go.sum
index afd2e7ca..6769a950 100644
--- a/go.sum
+++ b/go.sum
@@ -422,6 +422,8 @@ github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0a
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
github.com/golangci/golangci-lint/v2 v2.11.1 h1:aGbjflzzKNIdOoq/NawrhFjYpkNY4WzPSeIp2zBbzG8=
github.com/golangci/golangci-lint/v2 v2.11.1/go.mod h1:wexdFBIQNhHNhDe1oqzlGFE5dYUqlfccWJKWjoWF1GI=
+github.com/golangci/golangci-lint/v2 v2.11.2 h1:4Icd3mEqthcFcFww8L67OBtfKB/obXxko8aFUMqP/5w=
+github.com/golangci/golangci-lint/v2 v2.11.2/go.mod h1:wexdFBIQNhHNhDe1oqzlGFE5dYUqlfccWJKWjoWF1GI=
github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=
github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=
@@ -755,10 +757,6 @@ github.com/osapi-io/nats-client v0.0.0-20260306210421-d68b2a0f287b h1:d68ZLQLxJW
github.com/osapi-io/nats-client v0.0.0-20260306210421-d68b2a0f287b/go.mod h1:66M9jRN03gZezKNttR17FCRZyLdF7E0BvBLitfrJl38=
github.com/osapi-io/nats-server v0.0.0-20260216201410-1f33dfc63848 h1:ELW1sTVBn5JIc17mHgd5fhpO3/7btaxJpxykG2Fe0U4=
github.com/osapi-io/nats-server v0.0.0-20260216201410-1f33dfc63848/go.mod h1:4rzeY9jiJF/+Ej4WNwqK5HQ2sflZrEs60GxQpg3Iya8=
-github.com/osapi-io/osapi-sdk v0.0.0-20260307073158-439e543a3013 h1:kcP1brAYrbrETk+8jgJKyGE8NI0zIvSg3hT5Y1oviT4=
-github.com/osapi-io/osapi-sdk v0.0.0-20260307073158-439e543a3013/go.mod h1:i9g4jaIL6NVo9MRpz33lAEnY4L7u6aO97/5hN4W3hGE=
-github.com/osapi-io/osapi-sdk v0.0.0-20260307192743-857786ce1c9e h1:sCg9f0Undm5zUdZ+oKESdplMhRvAlqmnMqKlyOoInX0=
-github.com/osapi-io/osapi-sdk v0.0.0-20260307192743-857786ce1c9e/go.mod h1:i9g4jaIL6NVo9MRpz33lAEnY4L7u6aO97/5hN4W3hGE=
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
diff --git a/internal/audit/export/export_public_test.go b/internal/audit/export/export_public_test.go
index c1c7b692..a91f6f1a 100644
--- a/internal/audit/export/export_public_test.go
+++ b/internal/audit/export/export_public_test.go
@@ -27,7 +27,7 @@ import (
"testing"
"time"
- "github.com/osapi-io/osapi-sdk/pkg/osapi"
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
"github.com/stretchr/testify/suite"
"github.com/retr0h/osapi/internal/audit/export"
diff --git a/internal/audit/export/file.go b/internal/audit/export/file.go
index d765c2ca..491ca5bc 100644
--- a/internal/audit/export/file.go
+++ b/internal/audit/export/file.go
@@ -28,7 +28,7 @@ import (
"io"
"os"
- "github.com/osapi-io/osapi-sdk/pkg/osapi"
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
)
// marshalJSON is a package-level variable for testing the marshal error path.
diff --git a/internal/audit/export/file_public_test.go b/internal/audit/export/file_public_test.go
index 301d47e4..5bd1b423 100644
--- a/internal/audit/export/file_public_test.go
+++ b/internal/audit/export/file_public_test.go
@@ -31,7 +31,7 @@ import (
"testing"
"time"
- "github.com/osapi-io/osapi-sdk/pkg/osapi"
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
"github.com/stretchr/testify/suite"
"github.com/retr0h/osapi/internal/audit/export"
diff --git a/internal/audit/export/file_test.go b/internal/audit/export/file_test.go
index 0d21da4f..2cff9e24 100644
--- a/internal/audit/export/file_test.go
+++ b/internal/audit/export/file_test.go
@@ -28,7 +28,7 @@ import (
"testing"
"time"
- "github.com/osapi-io/osapi-sdk/pkg/osapi"
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
"github.com/stretchr/testify/suite"
)
diff --git a/internal/audit/export/types.go b/internal/audit/export/types.go
index cf7371c4..e6fc85c4 100644
--- a/internal/audit/export/types.go
+++ b/internal/audit/export/types.go
@@ -23,7 +23,7 @@ package export
import (
"context"
- "github.com/osapi-io/osapi-sdk/pkg/osapi"
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
)
// Exporter writes audit entries to a backend.
diff --git a/internal/cli/ui.go b/internal/cli/ui.go
index 930bb833..ef2fafa9 100644
--- a/internal/cli/ui.go
+++ b/internal/cli/ui.go
@@ -31,7 +31,7 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/google/uuid"
- "github.com/osapi-io/osapi-sdk/pkg/osapi"
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
)
// Theme colors for terminal UI rendering.
diff --git a/internal/cli/ui_public_test.go b/internal/cli/ui_public_test.go
index 840d251e..c9020ee4 100644
--- a/internal/cli/ui_public_test.go
+++ b/internal/cli/ui_public_test.go
@@ -31,7 +31,7 @@ import (
"time"
"github.com/google/uuid"
- "github.com/osapi-io/osapi-sdk/pkg/osapi"
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
diff --git a/pkg/sdk/osapi/agent.go b/pkg/sdk/osapi/agent.go
new file mode 100644
index 00000000..6968a1d3
--- /dev/null
+++ b/pkg/sdk/osapi/agent.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 osapi
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+// MessageResponse represents a simple message response from the API.
+type MessageResponse struct {
+ Message string
+}
+
+// AgentService provides agent discovery and details operations.
+type AgentService struct {
+ client *gen.ClientWithResponses
+}
+
+// List retrieves all active agents.
+func (s *AgentService) List(
+ ctx context.Context,
+) (*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,
+) (*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
+}
+
+// Drain initiates draining of an agent, stopping it from accepting
+// new jobs while allowing in-flight jobs to complete.
+func (s *AgentService) Drain(
+ ctx context.Context,
+ hostname string,
+) (*Response[MessageResponse], error) {
+ resp, err := s.client.DrainAgentWithResponse(ctx, hostname)
+ if err != nil {
+ return nil, fmt.Errorf("drain agent %s: %w", hostname, err)
+ }
+
+ if err := checkError(resp.StatusCode(), resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON409); err != nil {
+ return nil, err
+ }
+
+ if resp.JSON200 == nil {
+ return nil, &UnexpectedStatusError{APIError{
+ StatusCode: resp.StatusCode(),
+ Message: "nil response body",
+ }}
+ }
+
+ msg := MessageResponse{
+ Message: resp.JSON200.Message,
+ }
+
+ return NewResponse(msg, resp.Body), nil
+}
+
+// Undrain resumes job acceptance on a drained agent.
+func (s *AgentService) Undrain(
+ ctx context.Context,
+ hostname string,
+) (*Response[MessageResponse], error) {
+ resp, err := s.client.UndrainAgentWithResponse(ctx, hostname)
+ if err != nil {
+ return nil, fmt.Errorf("undrain agent %s: %w", hostname, err)
+ }
+
+ if err := checkError(resp.StatusCode(), resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON409); err != nil {
+ return nil, err
+ }
+
+ if resp.JSON200 == nil {
+ return nil, &UnexpectedStatusError{APIError{
+ StatusCode: resp.StatusCode(),
+ Message: "nil response body",
+ }}
+ }
+
+ msg := MessageResponse{
+ Message: resp.JSON200.Message,
+ }
+
+ return NewResponse(msg, resp.Body), nil
+}
diff --git a/pkg/sdk/osapi/agent_public_test.go b/pkg/sdk/osapi/agent_public_test.go
new file mode 100644
index 00000000..989cd57f
--- /dev/null
+++ b/pkg/sdk/osapi/agent_public_test.go
@@ -0,0 +1,438 @@
+// 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 (
+ "context"
+ "errors"
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
+)
+
+type AgentPublicTestSuite struct {
+ suite.Suite
+
+ ctx context.Context
+}
+
+func (suite *AgentPublicTestSuite) SetupTest() {
+ suite.ctx = context.Background()
+}
+
+func (suite *AgentPublicTestSuite) TestList() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ validateFunc func(*osapi.Response[osapi.AgentList], error)
+ }{
+ {
+ name: "when requesting agents returns no error",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"agents":[],"total":0}`))
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 401 returns AuthError",
+ handler: 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",
+ handler: 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.handler != nil {
+ server := httptest.NewServer(tc.handler)
+ 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)
+ })
+ }
+}
+
+func (suite *AgentPublicTestSuite) TestGet() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ hostname string
+ validateFunc func(*osapi.Response[osapi.Agent], error)
+ }{
+ {
+ name: "when requesting agent details returns no error",
+ hostname: "server1",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"hostname":"server1","status":"Ready"}`))
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 404 returns NotFoundError",
+ hostname: "unknown-host",
+ handler: 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",
+ hostname: "unknown-host",
+ 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",
+ hostname: "unknown-host",
+ handler: 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.handler != nil {
+ server := httptest.NewServer(tc.handler)
+ defer server.Close()
+ url = server.URL
+ }
+
+ sut := osapi.New(
+ url,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Agent.Get(suite.ctx, tc.hostname)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *AgentPublicTestSuite) TestDrain() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ hostname string
+ validateFunc func(*osapi.Response[osapi.MessageResponse], error)
+ }{
+ {
+ name: "when draining agent returns success",
+ hostname: "server1",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"message":"drain initiated for agent server1"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.MessageResponse], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.Equal("drain initiated for agent server1", resp.Data.Message)
+ },
+ },
+ {
+ name: "when server returns 409 returns ConflictError",
+ hostname: "test-host",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusConflict)
+ _, _ = w.Write([]byte(`{"error":"agent already draining"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.MessageResponse], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.ConflictError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusConflict, target.StatusCode)
+ suite.Equal("agent already draining", target.Message)
+ },
+ },
+ {
+ name: "when server returns 404 returns NotFoundError",
+ hostname: "test-host",
+ handler: 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.MessageResponse], 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 client HTTP error returns wrapped error",
+ hostname: "test-host",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.MessageResponse], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "drain agent")
+ },
+ },
+ {
+ name: "when response JSON200 is nil returns UnexpectedStatusError",
+ hostname: "test-host",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusOK)
+ },
+ validateFunc: func(resp *osapi.Response[osapi.MessageResponse], 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.handler != nil {
+ server := httptest.NewServer(tc.handler)
+ defer server.Close()
+ url = server.URL
+ }
+
+ sut := osapi.New(
+ url,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Agent.Drain(suite.ctx, tc.hostname)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *AgentPublicTestSuite) TestUndrain() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ hostname string
+ validateFunc func(*osapi.Response[osapi.MessageResponse], error)
+ }{
+ {
+ name: "when undraining agent returns success",
+ hostname: "server1",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"message":"undrain initiated for agent server1"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.MessageResponse], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.Equal("undrain initiated for agent server1", resp.Data.Message)
+ },
+ },
+ {
+ name: "when server returns 409 returns ConflictError",
+ hostname: "test-host",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusConflict)
+ _, _ = w.Write([]byte(`{"error":"agent not in draining state"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.MessageResponse], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.ConflictError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusConflict, target.StatusCode)
+ suite.Equal("agent not in draining state", target.Message)
+ },
+ },
+ {
+ name: "when server returns 404 returns NotFoundError",
+ hostname: "test-host",
+ handler: 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.MessageResponse], 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 client HTTP error returns wrapped error",
+ hostname: "test-host",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.MessageResponse], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "undrain agent")
+ },
+ },
+ {
+ name: "when response JSON200 is nil returns UnexpectedStatusError",
+ hostname: "test-host",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusOK)
+ },
+ validateFunc: func(resp *osapi.Response[osapi.MessageResponse], 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.handler != nil {
+ server := httptest.NewServer(tc.handler)
+ defer server.Close()
+ url = server.URL
+ }
+
+ sut := osapi.New(
+ url,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Agent.Undrain(suite.ctx, tc.hostname)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func TestAgentPublicTestSuite(t *testing.T) {
+ suite.Run(t, new(AgentPublicTestSuite))
+}
diff --git a/pkg/sdk/osapi/agent_types.go b/pkg/sdk/osapi/agent_types.go
new file mode 100644
index 00000000..d7f9b052
--- /dev/null
+++ b/pkg/sdk/osapi/agent_types.go
@@ -0,0 +1,290 @@
+// 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/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+// Agent represents a registered OSAPI agent.
+type Agent struct {
+ Hostname string
+ Status string
+ State string
+ Labels map[string]string
+ Architecture string
+ CPUCount int
+ Fqdn string
+ KernelVersion string
+ PackageMgr string
+ ServiceMgr string
+ LoadAverage *LoadAverage
+ Memory *Memory
+ OSInfo *OSInfo
+ PrimaryInterface string
+ Interfaces []NetworkInterface
+ Routes []Route
+ Conditions []Condition
+ Timeline []TimelineEvent
+ Uptime string
+ StartedAt time.Time
+ RegisteredAt time.Time
+ Facts map[string]any
+}
+
+// Condition represents a node condition evaluated agent-side.
+type Condition struct {
+ Type string
+ Status bool
+ Reason string
+ LastTransitionTime time.Time
+}
+
+// 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
+}
+
+// Route represents a network routing table entry.
+type Route struct {
+ Destination string
+ Gateway string
+ Interface string
+ Mask string
+ Flags string
+ Metric int
+}
+
+// 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.PrimaryInterface != nil {
+ a.PrimaryInterface = *g.PrimaryInterface
+ }
+
+ if g.Routes != nil {
+ routes := make([]Route, 0, len(*g.Routes))
+ for _, r := range *g.Routes {
+ route := Route{
+ Destination: r.Destination,
+ Gateway: r.Gateway,
+ Interface: r.Interface,
+ }
+
+ if r.Mask != nil {
+ route.Mask = *r.Mask
+ }
+
+ if r.Flags != nil {
+ route.Flags = *r.Flags
+ }
+
+ if r.Metric != nil {
+ route.Metric = *r.Metric
+ }
+
+ routes = append(routes, route)
+ }
+
+ a.Routes = routes
+ }
+
+ 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
+ }
+
+ if g.State != nil {
+ a.State = string(*g.State)
+ }
+
+ if g.Conditions != nil {
+ conditions := make([]Condition, 0, len(*g.Conditions))
+ for _, c := range *g.Conditions {
+ cond := Condition{
+ Type: string(c.Type),
+ Status: c.Status,
+ LastTransitionTime: c.LastTransitionTime,
+ }
+
+ if c.Reason != nil {
+ cond.Reason = *c.Reason
+ }
+
+ conditions = append(conditions, cond)
+ }
+
+ a.Conditions = conditions
+ }
+
+ if g.Timeline != nil {
+ timeline := make([]TimelineEvent, 0, len(*g.Timeline))
+ for _, t := range *g.Timeline {
+ te := TimelineEvent{
+ Event: t.Event,
+ Timestamp: t.Timestamp.Format(time.RFC3339),
+ }
+
+ if t.Hostname != nil {
+ te.Hostname = *t.Hostname
+ }
+
+ if t.Message != nil {
+ te.Message = *t.Message
+ }
+
+ if t.Error != nil {
+ te.Error = *t.Error
+ }
+
+ timeline = append(timeline, te)
+ }
+
+ a.Timeline = timeline
+ }
+
+ 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/sdk/osapi/agent_types_test.go b/pkg/sdk/osapi/agent_types_test.go
new file mode 100644
index 00000000..e638c11f
--- /dev/null
+++ b/pkg/sdk/osapi/agent_types_test.go
@@ -0,0 +1,325 @@
+// 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/retr0h/osapi/pkg/sdk/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"
+ primaryIface := "eth0"
+ routeMask := "255.255.255.0"
+ routeFlags := "UG"
+ routeMetric := 100
+ 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"}
+ state := gen.AgentInfoStateReady
+ reason := "load avg 0.50 < 4.00"
+ condTime := now.Add(-30 * time.Minute)
+ hostname := "web-01"
+ message := "agent started"
+ errMsg := "connection lost"
+
+ 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",
+ },
+ PrimaryInterface: &primaryIface,
+ Interfaces: &[]gen.NetworkInterfaceResponse{
+ {
+ Name: "eth0",
+ Family: &family,
+ Ipv4: &ipv4,
+ Ipv6: &ipv6,
+ Mac: &mac,
+ },
+ },
+ Routes: &[]gen.RouteResponse{
+ {
+ Destination: "0.0.0.0",
+ Gateway: "192.168.1.1",
+ Interface: "eth0",
+ Mask: &routeMask,
+ Flags: &routeFlags,
+ Metric: &routeMetric,
+ },
+ },
+ Uptime: &uptime,
+ StartedAt: &startedAt,
+ RegisteredAt: &now,
+ Facts: &facts,
+ State: &state,
+ Conditions: &[]gen.NodeCondition{
+ {
+ Type: gen.HighLoad,
+ Status: false,
+ Reason: &reason,
+ LastTransitionTime: condTime,
+ },
+ },
+ Timeline: &[]gen.TimelineEvent{
+ {
+ Timestamp: startedAt,
+ Event: "AgentStarted",
+ Hostname: &hostname,
+ Message: &message,
+ },
+ {
+ Timestamp: now,
+ Event: "AgentFailed",
+ Hostname: &hostname,
+ Error: &errMsg,
+ },
+ },
+ }
+ }(),
+ validateFunc: func(a Agent) {
+ suite.Equal("web-01", a.Hostname)
+ suite.Equal("Ready", a.Status)
+ suite.Equal("Ready", a.State)
+ 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.Equal("eth0", a.PrimaryInterface)
+
+ 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.Require().Len(a.Routes, 1)
+ suite.Equal("0.0.0.0", a.Routes[0].Destination)
+ suite.Equal("192.168.1.1", a.Routes[0].Gateway)
+ suite.Equal("eth0", a.Routes[0].Interface)
+ suite.Equal("255.255.255.0", a.Routes[0].Mask)
+ suite.Equal("UG", a.Routes[0].Flags)
+ suite.Equal(100, a.Routes[0].Metric)
+
+ 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)
+
+ suite.Require().Len(a.Conditions, 1)
+ suite.Equal("HighLoad", a.Conditions[0].Type)
+ suite.False(a.Conditions[0].Status)
+ suite.Equal("load avg 0.50 < 4.00", a.Conditions[0].Reason)
+ suite.Equal(
+ now.Add(-30*time.Minute),
+ a.Conditions[0].LastTransitionTime,
+ )
+
+ suite.Require().Len(a.Timeline, 2)
+ suite.Equal("AgentStarted", a.Timeline[0].Event)
+ suite.Equal(startedAt.Format(time.RFC3339), a.Timeline[0].Timestamp)
+ suite.Equal("web-01", a.Timeline[0].Hostname)
+ suite.Equal("agent started", a.Timeline[0].Message)
+ suite.Empty(a.Timeline[0].Error)
+
+ suite.Equal("AgentFailed", a.Timeline[1].Event)
+ suite.Equal(now.Format(time.RFC3339), a.Timeline[1].Timestamp)
+ suite.Equal("web-01", a.Timeline[1].Hostname)
+ suite.Empty(a.Timeline[1].Message)
+ suite.Equal("connection lost", a.Timeline[1].Error)
+ },
+ },
+ {
+ 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.Empty(a.State)
+ 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.Empty(a.PrimaryInterface)
+ suite.Nil(a.Interfaces)
+ suite.Nil(a.Routes)
+ suite.Nil(a.Conditions)
+ suite.Nil(a.Timeline)
+ 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/sdk/osapi/audit.go b/pkg/sdk/osapi/audit.go
new file mode 100644
index 00000000..b06e3e2d
--- /dev/null
+++ b/pkg/sdk/osapi/audit.go
@@ -0,0 +1,134 @@
+// 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 (
+ "context"
+ "fmt"
+
+ "github.com/google/uuid"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+// AuditService provides audit log operations.
+type AuditService struct {
+ client *gen.ClientWithResponses
+}
+
+// List retrieves audit log entries with pagination.
+func (s *AuditService) List(
+ ctx context.Context,
+ limit int,
+ offset int,
+) (*Response[AuditList], error) {
+ params := &gen.GetAuditLogsParams{
+ Limit: &limit,
+ Offset: &offset,
+ }
+
+ 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,
+) (*Response[AuditEntry], error) {
+ parsedID, err := uuid.Parse(id)
+ if err != nil {
+ return nil, err
+ }
+
+ 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,
+) (*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/sdk/osapi/audit_public_test.go b/pkg/sdk/osapi/audit_public_test.go
new file mode 100644
index 00000000..1318d9ef
--- /dev/null
+++ b/pkg/sdk/osapi/audit_public_test.go
@@ -0,0 +1,341 @@
+// 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 (
+ "context"
+ "errors"
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
+)
+
+type AuditPublicTestSuite struct {
+ suite.Suite
+
+ ctx context.Context
+}
+
+func (suite *AuditPublicTestSuite) SetupTest() {
+ suite.ctx = context.Background()
+}
+
+func (suite *AuditPublicTestSuite) TestList() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ limit int
+ offset int
+ validateFunc func(*osapi.Response[osapi.AuditList], error)
+ }{
+ {
+ name: "when listing audit entries returns audit list",
+ limit: 20,
+ offset: 0,
+ handler: 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}`))
+ }),
+ 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)
+ },
+ },
+ {
+ name: "when server returns 401 returns AuthError",
+ limit: 20,
+ offset: 0,
+ 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.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 client HTTP request fails returns error",
+ limit: 20,
+ offset: 0,
+ serverURL: "http://127.0.0.1:0",
+ 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",
+ limit: 20,
+ offset: 0,
+ 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.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() {
+ 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.Audit.List(suite.ctx, tc.limit, tc.offset)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *AuditPublicTestSuite) TestGet() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ id string
+ validateFunc func(*osapi.Response[osapi.AuditEntry], error)
+ }{
+ {
+ name: "when valid UUID returns audit entry",
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ handler: 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"}}`,
+ ),
+ )
+ }),
+ 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",
+ handler: 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"}}`,
+ ),
+ )
+ }),
+ validateFunc: func(resp *osapi.Response[osapi.AuditEntry], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ },
+ },
+ {
+ name: "when server returns 404 returns NotFoundError",
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ handler: 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 client HTTP request fails returns error",
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ serverURL: "http://127.0.0.1:0",
+ 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",
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ 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.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() {
+ 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.Audit.Get(suite.ctx, tc.id)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *AuditPublicTestSuite) TestExport() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ validateFunc func(*osapi.Response[osapi.AuditList], error)
+ }{
+ {
+ name: "when exporting audit entries returns audit list",
+ handler: 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}`))
+ }),
+ 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)
+ },
+ },
+ {
+ 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.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 client HTTP request fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ 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",
+ 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.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() {
+ 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.Audit.Export(suite.ctx)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func TestAuditPublicTestSuite(t *testing.T) {
+ suite.Run(t, new(AuditPublicTestSuite))
+}
diff --git a/pkg/sdk/osapi/audit_types.go b/pkg/sdk/osapi/audit_types.go
new file mode 100644
index 00000000..3a78de99
--- /dev/null
+++ b/pkg/sdk/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/retr0h/osapi/pkg/sdk/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/sdk/osapi/audit_types_test.go b/pkg/sdk/osapi/audit_types_test.go
new file mode 100644
index 00000000..313b1ad3
--- /dev/null
+++ b/pkg/sdk/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/retr0h/osapi/pkg/sdk/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/sdk/osapi/errors.go b/pkg/sdk/osapi/errors.go
new file mode 100644
index 00000000..508e7421
--- /dev/null
+++ b/pkg/sdk/osapi/errors.go
@@ -0,0 +1,98 @@
+// 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
+}
+
+// ConflictError represents conflict errors (409).
+type ConflictError struct {
+ APIError
+}
+
+// Unwrap returns the underlying APIError.
+func (e *ConflictError) 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/sdk/osapi/errors_public_test.go b/pkg/sdk/osapi/errors_public_test.go
new file mode 100644
index 00000000..e3f8cbc7
--- /dev/null
+++ b/pkg/sdk/osapi/errors_public_test.go
@@ -0,0 +1,367 @@
+// 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/retr0h/osapi/pkg/sdk/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 ConflictError formats correctly",
+ err: &osapi.ConflictError{
+ APIError: osapi.APIError{
+ StatusCode: 409,
+ Message: "already draining",
+ },
+ },
+ validateFunc: func(err error) {
+ suite.Equal(
+ "api error (status 409): already draining",
+ 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 ConflictError is unwrapped via errors.As",
+ err: fmt.Errorf("wrapped: %w", &osapi.ConflictError{
+ APIError: osapi.APIError{
+ StatusCode: 409,
+ Message: "already draining",
+ },
+ }),
+ validateFunc: func(err error) {
+ var target *osapi.ConflictError
+ suite.True(errors.As(err, &target))
+ suite.Equal(409, target.StatusCode)
+ suite.Equal("already draining", 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 ConflictError is matchable as APIError",
+ err: fmt.Errorf("wrapped: %w", &osapi.ConflictError{
+ APIError: osapi.APIError{
+ StatusCode: 409,
+ Message: "conflict",
+ },
+ }),
+ validateFunc: func(err error) {
+ var target *osapi.APIError
+ suite.True(errors.As(err, &target))
+ suite.Equal(409, target.StatusCode)
+ suite.Equal("conflict", 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/sdk/osapi/file.go b/pkg/sdk/osapi/file.go
new file mode 100644
index 00000000..b5121121
--- /dev/null
+++ b/pkg/sdk/osapi/file.go
@@ -0,0 +1,270 @@
+// 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 (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "errors"
+ "fmt"
+ "io"
+ "mime/multipart"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+// UploadOption configures Upload behavior.
+type UploadOption func(*uploadOptions)
+
+type uploadOptions struct {
+ force bool
+}
+
+// WithForce bypasses both SDK-side pre-check and server-side digest
+// check. The file is always uploaded and changed is always true.
+func WithForce() UploadOption {
+ return func(o *uploadOptions) { o.force = true }
+}
+
+// FileService provides file management operations for the Object Store.
+type FileService struct {
+ client *gen.ClientWithResponses
+}
+
+// Upload uploads a file to the Object Store via multipart/form-data.
+// By default, it computes SHA-256 locally and compares against the
+// stored hash to skip the upload when content is unchanged. Use
+// WithForce to bypass this check.
+func (s *FileService) Upload(
+ ctx context.Context,
+ name string,
+ contentType string,
+ file io.Reader,
+ opts ...UploadOption,
+) (*Response[FileUpload], error) {
+ var options uploadOptions
+ for _, o := range opts {
+ o(&options)
+ }
+
+ // Buffer file content for hashing and multipart construction.
+ fileData, err := io.ReadAll(file)
+ if err != nil {
+ return nil, fmt.Errorf("read file: %w", err)
+ }
+
+ // Compute SHA-256 locally.
+ hash := sha256.Sum256(fileData)
+ sha256Hex := fmt.Sprintf("%x", hash)
+
+ // SDK-side pre-check: skip upload if content unchanged.
+ // Skipped when force is set.
+ if !options.force {
+ existing, err := s.Get(ctx, name)
+ if err == nil && existing.Data.SHA256 == sha256Hex {
+ return NewResponse(FileUpload{
+ Name: name,
+ SHA256: sha256Hex,
+ Size: len(fileData),
+ Changed: false,
+ ContentType: contentType,
+ }, nil), nil
+ }
+ // On error (404, network, etc.) fall through to upload.
+ }
+
+ // Build multipart body from buffered content.
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+
+ _ = writer.WriteField("name", name)
+ _ = writer.WriteField("content_type", contentType)
+
+ part, _ := writer.CreateFormFile("file", name)
+ _, _ = part.Write(fileData)
+ _ = writer.Close()
+
+ // Pass force as query param.
+ params := &gen.PostFileParams{}
+ if options.force {
+ params.Force = &options.force
+ }
+
+ resp, err := s.client.PostFileWithBodyWithResponse(
+ ctx,
+ params,
+ writer.FormDataContentType(),
+ body,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("upload file: %w", err)
+ }
+
+ if err := checkError(
+ resp.StatusCode(),
+ resp.JSON400,
+ resp.JSON401,
+ resp.JSON403,
+ resp.JSON409,
+ resp.JSON500,
+ ); err != nil {
+ return nil, err
+ }
+
+ if resp.JSON201 == nil {
+ return nil, &UnexpectedStatusError{APIError{
+ StatusCode: resp.StatusCode(),
+ Message: "nil response body",
+ }}
+ }
+
+ return NewResponse(fileUploadFromGen(resp.JSON201), resp.Body), nil
+}
+
+// List retrieves all files stored in the Object Store.
+func (s *FileService) List(
+ ctx context.Context,
+) (*Response[FileList], error) {
+ resp, err := s.client.GetFilesWithResponse(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("list files: %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(fileListFromGen(resp.JSON200), resp.Body), nil
+}
+
+// Get retrieves metadata for a specific file in the Object Store.
+func (s *FileService) Get(
+ ctx context.Context,
+ name string,
+) (*Response[FileMetadata], error) {
+ resp, err := s.client.GetFileByNameWithResponse(ctx, name)
+ if err != nil {
+ return nil, fmt.Errorf("get file %s: %w", name, 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(fileMetadataFromGen(resp.JSON200), resp.Body), nil
+}
+
+// Delete removes a file from the Object Store.
+func (s *FileService) Delete(
+ ctx context.Context,
+ name string,
+) (*Response[FileDelete], error) {
+ resp, err := s.client.DeleteFileByNameWithResponse(ctx, name)
+ if err != nil {
+ return nil, fmt.Errorf("delete file %s: %w", name, 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(fileDeleteFromGen(resp.JSON200), resp.Body), nil
+}
+
+// Changed computes the SHA-256 of the provided content and compares
+// it against the stored hash in the Object Store. Returns true if
+// the content differs or the file does not exist yet.
+func (s *FileService) Changed(
+ ctx context.Context,
+ name string,
+ file io.Reader,
+) (*Response[FileChanged], error) {
+ fileData, err := io.ReadAll(file)
+ if err != nil {
+ return nil, fmt.Errorf("read file: %w", err)
+ }
+
+ hash := sha256.Sum256(fileData)
+ sha256Hex := fmt.Sprintf("%x", hash)
+
+ existing, err := s.Get(ctx, name)
+ if err != nil {
+ var notFound *NotFoundError
+ if errors.As(err, ¬Found) {
+ return NewResponse(FileChanged{
+ Name: name,
+ Changed: true,
+ SHA256: sha256Hex,
+ }, nil), nil
+ }
+
+ return nil, fmt.Errorf("check file %s: %w", name, err)
+ }
+
+ changed := existing.Data.SHA256 != sha256Hex
+
+ return NewResponse(FileChanged{
+ Name: name,
+ Changed: changed,
+ SHA256: sha256Hex,
+ }, nil), nil
+}
diff --git a/pkg/sdk/osapi/file_public_test.go b/pkg/sdk/osapi/file_public_test.go
new file mode 100644
index 00000000..43b64084
--- /dev/null
+++ b/pkg/sdk/osapi/file_public_test.go
@@ -0,0 +1,789 @@
+// 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 (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "errors"
+ "fmt"
+ "io"
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
+)
+
+type FilePublicTestSuite struct {
+ suite.Suite
+
+ ctx context.Context
+}
+
+func (suite *FilePublicTestSuite) SetupTest() {
+ suite.ctx = context.Background()
+}
+
+func (suite *FilePublicTestSuite) TestUpload() {
+ fileContent := []byte("content")
+ hash := sha256.Sum256(fileContent)
+ contentSHA := fmt.Sprintf("%x", hash)
+
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ file io.Reader
+ opts []osapi.UploadOption
+ validateFunc func(*osapi.Response[osapi.FileUpload], error)
+ }{
+ {
+ name: "when uploading new file returns result",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ if r.Method == http.MethodGet {
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"error":"file not found"}`))
+ return
+ }
+ w.WriteHeader(http.StatusCreated)
+ _, _ = w.Write(
+ []byte(
+ `{"name":"nginx.conf","sha256":"abc123","size":1024,"changed":true,"content_type":"raw"}`,
+ ),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileUpload], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.Equal("nginx.conf", resp.Data.Name)
+ suite.Equal("abc123", resp.Data.SHA256)
+ suite.Equal(1024, resp.Data.Size)
+ suite.True(resp.Data.Changed)
+ suite.Equal("raw", resp.Data.ContentType)
+ },
+ },
+ {
+ name: "when pre-check SHA matches skips upload",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ if r.Method == http.MethodGet {
+ w.WriteHeader(http.StatusOK)
+ _, _ = fmt.Fprintf(w,
+ `{"name":"nginx.conf","sha256":"%s","size":7,"content_type":"raw"}`,
+ contentSHA,
+ )
+ return
+ }
+ // POST should NOT be called — fail if it is.
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte(`{"error":"unexpected POST"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileUpload], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.Equal("nginx.conf", resp.Data.Name)
+ suite.Equal(contentSHA, resp.Data.SHA256)
+ suite.False(resp.Data.Changed)
+ suite.Nil(resp.RawJSON())
+ },
+ },
+ {
+ name: "when force skips pre-check and uploads",
+ opts: []osapi.UploadOption{osapi.WithForce()},
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ if r.Method == http.MethodGet {
+ // GET should NOT be called — fail if it is.
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte(`{"error":"unexpected GET"}`))
+ return
+ }
+ suite.Contains(r.URL.RawQuery, "force=true")
+ w.WriteHeader(http.StatusCreated)
+ _, _ = w.Write(
+ []byte(
+ `{"name":"nginx.conf","sha256":"abc123","size":7,"changed":true,"content_type":"raw"}`,
+ ),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileUpload], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.True(resp.Data.Changed)
+ },
+ },
+ {
+ name: "when server returns 409 returns ConflictError",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ if r.Method == http.MethodGet {
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"error":"file not found"}`))
+ return
+ }
+ w.WriteHeader(http.StatusConflict)
+ _, _ = w.Write([]byte(`{"error":"file already exists"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileUpload], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.ConflictError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusConflict, target.StatusCode)
+ },
+ },
+ {
+ name: "when server returns 400 returns ValidationError",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ if r.Method == http.MethodGet {
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"error":"not found"}`))
+ return
+ }
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"error":"name is required"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileUpload], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.ValidationError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusBadRequest, target.StatusCode)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ handler: 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.FileUpload], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.AuthError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusForbidden, target.StatusCode)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.FileUpload], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ },
+ },
+ {
+ name: "when server returns 201 with no JSON body returns UnexpectedStatusError",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodGet {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ w.WriteHeader(http.StatusCreated)
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileUpload], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.UnexpectedStatusError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusCreated, target.StatusCode)
+ suite.Equal("nil response body", target.Message)
+ },
+ },
+ {
+ name: "when file reader returns error",
+ file: &errReader{err: errors.New("read failed")},
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusCreated)
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileUpload], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "read file")
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ file := tc.file
+ if file == nil {
+ file = bytes.NewReader(fileContent)
+ }
+
+ resp, err := sut.File.Upload(
+ suite.ctx,
+ "nginx.conf",
+ "raw",
+ file,
+ tc.opts...,
+ )
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *FilePublicTestSuite) TestList() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ validateFunc func(*osapi.Response[osapi.FileList], error)
+ }{
+ {
+ name: "when listing files returns results",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(
+ []byte(
+ `{"files":[{"name":"file1.txt","sha256":"aaa","size":100,"content_type":"raw"}],"total":1}`,
+ ),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileList], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.Len(resp.Data.Files, 1)
+ suite.Equal(1, resp.Data.Total)
+ suite.Equal("file1.txt", resp.Data.Files[0].Name)
+ suite.Equal("raw", resp.Data.Files[0].ContentType)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ handler: 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.FileList], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.AuthError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusForbidden, target.StatusCode)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.FileList], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "list files")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileList], 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.File.List(suite.ctx)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *FilePublicTestSuite) TestGet() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ fileName string
+ validateFunc func(*osapi.Response[osapi.FileMetadata], error)
+ }{
+ {
+ name: "when getting file returns metadata",
+ fileName: "nginx.conf",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(
+ []byte(
+ `{"name":"nginx.conf","sha256":"def456","size":512,"content_type":"raw"}`,
+ ),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileMetadata], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.Equal("nginx.conf", resp.Data.Name)
+ suite.Equal("def456", resp.Data.SHA256)
+ suite.Equal(512, resp.Data.Size)
+ suite.Equal("raw", resp.Data.ContentType)
+ },
+ },
+ {
+ name: "when server returns 400 returns ValidationError",
+ fileName: "nginx.conf",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"error":"invalid file name"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileMetadata], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.ValidationError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusBadRequest, target.StatusCode)
+ },
+ },
+ {
+ name: "when server returns 404 returns NotFoundError",
+ fileName: "missing.conf",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"error":"file not found"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileMetadata], 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 server returns 403 returns AuthError",
+ fileName: "nginx.conf",
+ handler: 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.FileMetadata], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.AuthError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusForbidden, target.StatusCode)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ fileName: "nginx.conf",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.FileMetadata], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "get file nginx.conf")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ fileName: "nginx.conf",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileMetadata], 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.File.Get(suite.ctx, tc.fileName)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *FilePublicTestSuite) TestDelete() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ fileName string
+ validateFunc func(*osapi.Response[osapi.FileDelete], error)
+ }{
+ {
+ name: "when deleting file returns result",
+ fileName: "old.conf",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(
+ []byte(`{"name":"old.conf","deleted":true}`),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileDelete], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.Equal("old.conf", resp.Data.Name)
+ suite.True(resp.Data.Deleted)
+ },
+ },
+ {
+ name: "when server returns 400 returns ValidationError",
+ fileName: "old.conf",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"error":"invalid file name"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileDelete], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.ValidationError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusBadRequest, target.StatusCode)
+ },
+ },
+ {
+ name: "when server returns 404 returns NotFoundError",
+ fileName: "missing.conf",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"error":"file not found"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileDelete], 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 server returns 403 returns AuthError",
+ fileName: "old.conf",
+ handler: 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.FileDelete], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.AuthError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusForbidden, target.StatusCode)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ fileName: "old.conf",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.FileDelete], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "delete file old.conf")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ fileName: "old.conf",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileDelete], 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.File.Delete(suite.ctx, tc.fileName)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *FilePublicTestSuite) TestChanged() {
+ fileContent := []byte("content")
+ hash := sha256.Sum256(fileContent)
+ contentSHA := fmt.Sprintf("%x", hash)
+
+ differentContent := []byte("different")
+ diffHash := sha256.Sum256(differentContent)
+ diffSHA := fmt.Sprintf("%x", diffHash)
+
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ file io.Reader
+ validateFunc func(*osapi.Response[osapi.FileChanged], error)
+ }{
+ {
+ name: "when file does not exist returns changed true",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"error":"file not found"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileChanged], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.True(resp.Data.Changed)
+ suite.Equal("nginx.conf", resp.Data.Name)
+ suite.Equal(contentSHA, resp.Data.SHA256)
+ },
+ },
+ {
+ name: "when SHA matches returns changed false",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = fmt.Fprintf(w,
+ `{"name":"nginx.conf","sha256":"%s","size":7,"content_type":"raw"}`,
+ contentSHA,
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileChanged], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.False(resp.Data.Changed)
+ suite.Equal(contentSHA, resp.Data.SHA256)
+ },
+ },
+ {
+ name: "when SHA differs returns changed true",
+ file: bytes.NewReader(differentContent),
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = fmt.Fprintf(w,
+ `{"name":"nginx.conf","sha256":"%s","size":7,"content_type":"raw"}`,
+ contentSHA,
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileChanged], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.True(resp.Data.Changed)
+ suite.Equal(diffSHA, resp.Data.SHA256)
+ },
+ },
+ {
+ name: "when server returns 403 returns error",
+ handler: 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.FileChanged], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "check file nginx.conf")
+ },
+ },
+ {
+ name: "when file reader returns error",
+ file: &errReader{err: errors.New("read failed")},
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileChanged], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "read file")
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ file := tc.file
+ if file == nil {
+ file = bytes.NewReader(fileContent)
+ }
+
+ resp, err := sut.File.Changed(
+ suite.ctx,
+ "nginx.conf",
+ file,
+ )
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+type errReader struct {
+ err error
+}
+
+func (r *errReader) Read(
+ _ []byte,
+) (int, error) {
+ return 0, r.err
+}
+
+func TestFilePublicTestSuite(t *testing.T) {
+ suite.Run(t, new(FilePublicTestSuite))
+}
diff --git a/pkg/sdk/osapi/file_types.go b/pkg/sdk/osapi/file_types.go
new file mode 100644
index 00000000..1ba65683
--- /dev/null
+++ b/pkg/sdk/osapi/file_types.go
@@ -0,0 +1,167 @@
+// 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/retr0h/osapi/pkg/sdk/osapi/gen"
+
+// FileUpload represents a successfully uploaded file.
+type FileUpload struct {
+ Name string
+ SHA256 string
+ Size int
+ Changed bool
+ ContentType string
+}
+
+// FileItem represents file metadata in a list.
+type FileItem struct {
+ Name string
+ SHA256 string
+ Size int
+ ContentType string
+}
+
+// FileList is a collection of files with total count.
+type FileList struct {
+ Files []FileItem
+ Total int
+}
+
+// FileMetadata represents metadata for a single file.
+type FileMetadata struct {
+ Name string
+ SHA256 string
+ Size int
+ ContentType string
+}
+
+// FileDelete represents the result of a file deletion.
+type FileDelete struct {
+ Name string
+ Deleted bool
+}
+
+// FileChanged represents the result of a change detection check.
+type FileChanged struct {
+ Name string
+ Changed bool
+ SHA256 string
+}
+
+// FileDeployResult represents the result of a file deploy operation.
+type FileDeployResult struct {
+ JobID string
+ Hostname string
+ Changed bool
+}
+
+// FileStatusResult represents the result of a file status check.
+type FileStatusResult struct {
+ JobID string
+ Hostname string
+ Path string
+ Status string
+ SHA256 string
+}
+
+// fileUploadFromGen converts a gen.FileUploadResponse to a FileUpload.
+func fileUploadFromGen(
+ g *gen.FileUploadResponse,
+) FileUpload {
+ return FileUpload{
+ Name: g.Name,
+ SHA256: g.Sha256,
+ Size: g.Size,
+ Changed: g.Changed,
+ ContentType: g.ContentType,
+ }
+}
+
+// fileListFromGen converts a gen.FileListResponse to a FileList.
+func fileListFromGen(
+ g *gen.FileListResponse,
+) FileList {
+ files := make([]FileItem, 0, len(g.Files))
+ for _, f := range g.Files {
+ files = append(files, FileItem{
+ Name: f.Name,
+ SHA256: f.Sha256,
+ Size: f.Size,
+ ContentType: f.ContentType,
+ })
+ }
+
+ return FileList{
+ Files: files,
+ Total: g.Total,
+ }
+}
+
+// fileMetadataFromGen converts a gen.FileInfoResponse to a FileMetadata.
+func fileMetadataFromGen(
+ g *gen.FileInfoResponse,
+) FileMetadata {
+ return FileMetadata{
+ Name: g.Name,
+ SHA256: g.Sha256,
+ Size: g.Size,
+ ContentType: g.ContentType,
+ }
+}
+
+// fileDeleteFromGen converts a gen.FileDeleteResponse to a FileDelete.
+func fileDeleteFromGen(
+ g *gen.FileDeleteResponse,
+) FileDelete {
+ return FileDelete{
+ Name: g.Name,
+ Deleted: g.Deleted,
+ }
+}
+
+// fileDeployResultFromGen converts a gen.FileDeployResponse to a FileDeployResult.
+func fileDeployResultFromGen(
+ g *gen.FileDeployResponse,
+) FileDeployResult {
+ return FileDeployResult{
+ JobID: g.JobId,
+ Hostname: g.Hostname,
+ Changed: g.Changed,
+ }
+}
+
+// fileStatusResultFromGen converts a gen.FileStatusResponse to a FileStatusResult.
+func fileStatusResultFromGen(
+ g *gen.FileStatusResponse,
+) FileStatusResult {
+ r := FileStatusResult{
+ JobID: g.JobId,
+ Hostname: g.Hostname,
+ Path: g.Path,
+ Status: g.Status,
+ }
+
+ if g.Sha256 != nil {
+ r.SHA256 = *g.Sha256
+ }
+
+ return r
+}
diff --git a/pkg/sdk/osapi/file_types_test.go b/pkg/sdk/osapi/file_types_test.go
new file mode 100644
index 00000000..1e31370b
--- /dev/null
+++ b/pkg/sdk/osapi/file_types_test.go
@@ -0,0 +1,264 @@
+// 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/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+type FileTypesTestSuite struct {
+ suite.Suite
+}
+
+func (suite *FileTypesTestSuite) TestFileUploadFromGen() {
+ tests := []struct {
+ name string
+ input *gen.FileUploadResponse
+ validateFunc func(FileUpload)
+ }{
+ {
+ name: "when all fields populated returns FileUpload",
+ input: &gen.FileUploadResponse{
+ Name: "nginx.conf",
+ Sha256: "abc123",
+ Size: 1024,
+ Changed: true,
+ ContentType: "raw",
+ },
+ validateFunc: func(result FileUpload) {
+ suite.Equal("nginx.conf", result.Name)
+ suite.Equal("abc123", result.SHA256)
+ suite.Equal(1024, result.Size)
+ suite.True(result.Changed)
+ suite.Equal("raw", result.ContentType)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ result := fileUploadFromGen(tc.input)
+ tc.validateFunc(result)
+ })
+ }
+}
+
+func (suite *FileTypesTestSuite) TestFileListFromGen() {
+ tests := []struct {
+ name string
+ input *gen.FileListResponse
+ validateFunc func(FileList)
+ }{
+ {
+ name: "when files exist returns FileList with items",
+ input: &gen.FileListResponse{
+ Files: []gen.FileInfo{
+ {Name: "file1.txt", Sha256: "aaa", Size: 100, ContentType: "raw"},
+ {Name: "file2.txt", Sha256: "bbb", Size: 200, ContentType: "template"},
+ },
+ Total: 2,
+ },
+ validateFunc: func(result FileList) {
+ suite.Len(result.Files, 2)
+ suite.Equal(2, result.Total)
+ suite.Equal("file1.txt", result.Files[0].Name)
+ suite.Equal("aaa", result.Files[0].SHA256)
+ suite.Equal(100, result.Files[0].Size)
+ suite.Equal("raw", result.Files[0].ContentType)
+ suite.Equal("file2.txt", result.Files[1].Name)
+ suite.Equal("template", result.Files[1].ContentType)
+ },
+ },
+ {
+ name: "when no files returns empty FileList",
+ input: &gen.FileListResponse{
+ Files: []gen.FileInfo{},
+ Total: 0,
+ },
+ validateFunc: func(result FileList) {
+ suite.Empty(result.Files)
+ suite.Equal(0, result.Total)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ result := fileListFromGen(tc.input)
+ tc.validateFunc(result)
+ })
+ }
+}
+
+func (suite *FileTypesTestSuite) TestFileMetadataFromGen() {
+ tests := []struct {
+ name string
+ input *gen.FileInfoResponse
+ validateFunc func(FileMetadata)
+ }{
+ {
+ name: "when all fields populated returns FileMetadata",
+ input: &gen.FileInfoResponse{
+ Name: "config.yaml",
+ Sha256: "def456",
+ Size: 512,
+ ContentType: "template",
+ },
+ validateFunc: func(result FileMetadata) {
+ suite.Equal("config.yaml", result.Name)
+ suite.Equal("def456", result.SHA256)
+ suite.Equal(512, result.Size)
+ suite.Equal("template", result.ContentType)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ result := fileMetadataFromGen(tc.input)
+ tc.validateFunc(result)
+ })
+ }
+}
+
+func (suite *FileTypesTestSuite) TestFileDeleteFromGen() {
+ tests := []struct {
+ name string
+ input *gen.FileDeleteResponse
+ validateFunc func(FileDelete)
+ }{
+ {
+ name: "when deleted returns FileDelete with true",
+ input: &gen.FileDeleteResponse{
+ Name: "old.conf",
+ Deleted: true,
+ },
+ validateFunc: func(result FileDelete) {
+ suite.Equal("old.conf", result.Name)
+ suite.True(result.Deleted)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ result := fileDeleteFromGen(tc.input)
+ tc.validateFunc(result)
+ })
+ }
+}
+
+func (suite *FileTypesTestSuite) TestFileDeployResultFromGen() {
+ tests := []struct {
+ name string
+ input *gen.FileDeployResponse
+ validateFunc func(FileDeployResult)
+ }{
+ {
+ name: "when all fields populated returns FileDeployResult",
+ input: &gen.FileDeployResponse{
+ JobId: "job-123",
+ Hostname: "web-01",
+ Changed: true,
+ },
+ validateFunc: func(result FileDeployResult) {
+ suite.Equal("job-123", result.JobID)
+ suite.Equal("web-01", result.Hostname)
+ suite.True(result.Changed)
+ },
+ },
+ {
+ name: "when not changed returns false",
+ input: &gen.FileDeployResponse{
+ JobId: "job-456",
+ Hostname: "web-02",
+ Changed: false,
+ },
+ validateFunc: func(result FileDeployResult) {
+ suite.False(result.Changed)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ result := fileDeployResultFromGen(tc.input)
+ tc.validateFunc(result)
+ })
+ }
+}
+
+func (suite *FileTypesTestSuite) TestFileStatusResultFromGen() {
+ sha := "abc123"
+
+ tests := []struct {
+ name string
+ input *gen.FileStatusResponse
+ validateFunc func(FileStatusResult)
+ }{
+ {
+ name: "when all fields populated returns FileStatusResult",
+ input: &gen.FileStatusResponse{
+ JobId: "job-789",
+ Hostname: "web-03",
+ Path: "/etc/nginx/nginx.conf",
+ Status: "in-sync",
+ Sha256: &sha,
+ },
+ validateFunc: func(result FileStatusResult) {
+ suite.Equal("job-789", result.JobID)
+ suite.Equal("web-03", result.Hostname)
+ suite.Equal("/etc/nginx/nginx.conf", result.Path)
+ suite.Equal("in-sync", result.Status)
+ suite.Equal("abc123", result.SHA256)
+ },
+ },
+ {
+ name: "when sha256 is nil returns empty string",
+ input: &gen.FileStatusResponse{
+ JobId: "job-000",
+ Hostname: "web-04",
+ Path: "/etc/missing.conf",
+ Status: "missing",
+ Sha256: nil,
+ },
+ validateFunc: func(result FileStatusResult) {
+ suite.Equal("missing", result.Status)
+ suite.Empty(result.SHA256)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ result := fileStatusResultFromGen(tc.input)
+ tc.validateFunc(result)
+ })
+ }
+}
+
+func TestFileTypesTestSuite(t *testing.T) {
+ suite.Run(t, new(FileTypesTestSuite))
+}
diff --git a/pkg/sdk/osapi/gen/cfg.yaml b/pkg/sdk/osapi/gen/cfg.yaml
new file mode 100644
index 00000000..692fbabd
--- /dev/null
+++ b/pkg/sdk/osapi/gen/cfg.yaml
@@ -0,0 +1,8 @@
+---
+package: gen
+output: client.gen.go
+generate:
+ models: true
+ client: true
+output-options:
+ skip-prune: true
diff --git a/pkg/sdk/osapi/gen/client.gen.go b/pkg/sdk/osapi/gen/client.gen.go
new file mode 100644
index 00000000..3a78e45c
--- /dev/null
+++ b/pkg/sdk/osapi/gen/client.gen.go
@@ -0,0 +1,6438 @@
+// Package gen provides primitives to interact with the openapi HTTP API.
+//
+// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT.
+package gen
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/oapi-codegen/runtime"
+ openapi_types "github.com/oapi-codegen/runtime/types"
+)
+
+const (
+ BearerAuthScopes = "BearerAuth.Scopes"
+)
+
+// Defines values for AgentInfoState.
+const (
+ AgentInfoStateCordoned AgentInfoState = "Cordoned"
+ AgentInfoStateDraining AgentInfoState = "Draining"
+ AgentInfoStateReady AgentInfoState = "Ready"
+)
+
+// Defines values for AgentInfoStatus.
+const (
+ AgentInfoStatusNotReady AgentInfoStatus = "NotReady"
+ AgentInfoStatusReady AgentInfoStatus = "Ready"
+)
+
+// Defines values for DNSUpdateResultItemStatus.
+const (
+ DNSUpdateResultItemStatusFailed DNSUpdateResultItemStatus = "failed"
+ DNSUpdateResultItemStatusOk DNSUpdateResultItemStatus = "ok"
+)
+
+// Defines values for FileDeployRequestContentType.
+const (
+ FileDeployRequestContentTypeRaw FileDeployRequestContentType = "raw"
+ FileDeployRequestContentTypeTemplate FileDeployRequestContentType = "template"
+)
+
+// Defines values for NetworkInterfaceResponseFamily.
+const (
+ Dual NetworkInterfaceResponseFamily = "dual"
+ Inet NetworkInterfaceResponseFamily = "inet"
+ Inet6 NetworkInterfaceResponseFamily = "inet6"
+)
+
+// Defines values for NodeConditionType.
+const (
+ DiskPressure NodeConditionType = "DiskPressure"
+ HighLoad NodeConditionType = "HighLoad"
+ MemoryPressure NodeConditionType = "MemoryPressure"
+)
+
+// Defines values for PostFileMultipartBodyContentType.
+const (
+ PostFileMultipartBodyContentTypeRaw PostFileMultipartBodyContentType = "raw"
+ PostFileMultipartBodyContentTypeTemplate PostFileMultipartBodyContentType = "template"
+)
+
+// Defines values for GetJobParamsStatus.
+const (
+ GetJobParamsStatusCompleted GetJobParamsStatus = "completed"
+ GetJobParamsStatusFailed GetJobParamsStatus = "failed"
+ GetJobParamsStatusPartialFailure GetJobParamsStatus = "partial_failure"
+ GetJobParamsStatusProcessing GetJobParamsStatus = "processing"
+ GetJobParamsStatusSubmitted GetJobParamsStatus = "submitted"
+)
+
+// AgentDetail defines model for AgentDetail.
+type AgentDetail struct {
+ // Hostname Agent hostname.
+ Hostname string `json:"hostname"`
+
+ // Labels Formatted label string.
+ Labels *string `json:"labels,omitempty"`
+
+ // Registered Time since last heartbeat registration.
+ Registered string `json:"registered"`
+}
+
+// AgentInfo defines model for AgentInfo.
+type AgentInfo struct {
+ // Architecture CPU architecture.
+ Architecture *string `json:"architecture,omitempty"`
+
+ // Conditions Evaluated node conditions.
+ Conditions *[]NodeCondition `json:"conditions,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"`
+ 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"`
+
+ // LoadAverage The system load averages for 1, 5, and 15 minutes.
+ LoadAverage *LoadAverageResponse `json:"load_average,omitempty"`
+
+ // Memory Memory usage information.
+ Memory *MemoryResponse `json:"memory,omitempty"`
+
+ // OsInfo Operating system information.
+ OsInfo *OSInfoResponse `json:"os_info,omitempty"`
+
+ // PackageMgr Package manager.
+ PackageMgr *string `json:"package_mgr,omitempty"`
+
+ // PrimaryInterface Name of the interface used for the default route.
+ PrimaryInterface *string `json:"primary_interface,omitempty"`
+
+ // RegisteredAt When the agent last refreshed its heartbeat.
+ RegisteredAt *time.Time `json:"registered_at,omitempty"`
+
+ // Routes Network routing table entries.
+ Routes *[]RouteResponse `json:"routes,omitempty"`
+
+ // ServiceMgr Init system.
+ ServiceMgr *string `json:"service_mgr,omitempty"`
+
+ // StartedAt When the agent process started.
+ StartedAt *time.Time `json:"started_at,omitempty"`
+
+ // State Agent scheduling state.
+ State *AgentInfoState `json:"state,omitempty"`
+
+ // Status The current status of the agent.
+ Status AgentInfoStatus `json:"status"`
+
+ // Timeline Agent state transition history.
+ Timeline *[]TimelineEvent `json:"timeline,omitempty"`
+
+ // Uptime The system uptime.
+ Uptime *string `json:"uptime,omitempty"`
+}
+
+// AgentInfoState Agent scheduling state.
+type AgentInfoState string
+
+// AgentInfoStatus The current status of the agent.
+type AgentInfoStatus string
+
+// AgentStats defines model for AgentStats.
+type AgentStats struct {
+ // Agents Per-agent registration details.
+ Agents *[]AgentDetail `json:"agents,omitempty"`
+
+ // Ready Number of agents with Ready status.
+ Ready int `json:"ready"`
+
+ // Total Total number of registered agents.
+ Total int `json:"total"`
+}
+
+// AuditEntry defines model for AuditEntry.
+type AuditEntry struct {
+ // DurationMs Request duration in milliseconds.
+ DurationMs int64 `json:"duration_ms"`
+
+ // Id Unique identifier for the audit entry.
+ Id openapi_types.UUID `json:"id"`
+
+ // Method HTTP method.
+ Method string `json:"method"`
+
+ // OperationId OpenAPI operation ID.
+ OperationId *string `json:"operation_id,omitempty"`
+
+ // Path Request URL path.
+ Path string `json:"path"`
+
+ // ResponseCode HTTP response status code.
+ ResponseCode int `json:"response_code"`
+
+ // Roles Roles from the JWT token.
+ Roles []string `json:"roles"`
+
+ // SourceIp Client IP address.
+ SourceIp string `json:"source_ip"`
+
+ // Timestamp When the request was processed.
+ Timestamp time.Time `json:"timestamp"`
+
+ // User Authenticated user (JWT subject).
+ User string `json:"user"`
+}
+
+// AuditEntryResponse defines model for AuditEntryResponse.
+type AuditEntryResponse struct {
+ Entry AuditEntry `json:"entry"`
+}
+
+// CommandExecRequest defines model for CommandExecRequest.
+type CommandExecRequest struct {
+ // Args Command arguments.
+ Args *[]string `json:"args,omitempty"`
+
+ // Command The executable name or path.
+ Command string `json:"command" validate:"required,min=1"`
+
+ // Cwd Working directory for the command.
+ Cwd *string `json:"cwd,omitempty"`
+
+ // Timeout Timeout in seconds (default 30, max 300).
+ Timeout *int `json:"timeout,omitempty" validate:"omitempty,min=1,max=300"`
+}
+
+// CommandResultCollectionResponse defines model for CommandResultCollectionResponse.
+type CommandResultCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []CommandResultItem `json:"results"`
+}
+
+// CommandResultItem defines model for CommandResultItem.
+type CommandResultItem struct {
+ // Changed Whether the command modified system state.
+ Changed *bool `json:"changed,omitempty"`
+
+ // DurationMs Execution time in milliseconds.
+ DurationMs *int64 `json:"duration_ms,omitempty"`
+
+ // Error Error message if the agent failed.
+ Error *string `json:"error,omitempty"`
+
+ // ExitCode Exit code of the command.
+ ExitCode *int `json:"exit_code,omitempty"`
+
+ // Hostname The hostname of the agent that executed the command.
+ Hostname string `json:"hostname"`
+
+ // Stderr Standard error output of the command.
+ Stderr *string `json:"stderr,omitempty"`
+
+ // Stdout Standard output of the command.
+ Stdout *string `json:"stdout,omitempty"`
+}
+
+// CommandShellRequest defines model for CommandShellRequest.
+type CommandShellRequest struct {
+ // Command The full shell command string.
+ Command string `json:"command" validate:"required,min=1"`
+
+ // Cwd Working directory for the command.
+ Cwd *string `json:"cwd,omitempty"`
+
+ // Timeout Timeout in seconds (default 30, max 300).
+ Timeout *int `json:"timeout,omitempty" validate:"omitempty,min=1,max=300"`
+}
+
+// ComponentHealth defines model for ComponentHealth.
+type ComponentHealth struct {
+ // Error Error message when component is unhealthy.
+ Error *string `json:"error,omitempty"`
+
+ // Status Component health status.
+ Status string `json:"status"`
+}
+
+// ConsumerDetail defines model for ConsumerDetail.
+type ConsumerDetail struct {
+ // AckPending Messages delivered but not yet acknowledged.
+ AckPending int `json:"ack_pending"`
+
+ // Name Consumer name.
+ Name string `json:"name"`
+
+ // Pending Messages not yet delivered.
+ Pending int `json:"pending"`
+
+ // Redelivered Messages redelivered and not yet acknowledged.
+ Redelivered int `json:"redelivered"`
+}
+
+// ConsumerStats defines model for ConsumerStats.
+type ConsumerStats struct {
+ // Consumers Per-consumer details.
+ Consumers *[]ConsumerDetail `json:"consumers,omitempty"`
+
+ // Total Total number of JetStream consumers.
+ Total int `json:"total"`
+}
+
+// CreateJobRequest defines model for CreateJobRequest.
+type CreateJobRequest struct {
+ // Operation The operation to perform, as a JSON object.
+ Operation map[string]interface{} `json:"operation" validate:"required"`
+
+ // TargetHostname The target hostname for routing (_any, _all, or specific hostname).
+ TargetHostname string `json:"target_hostname" validate:"required,min=1,valid_target"`
+}
+
+// CreateJobResponse defines model for CreateJobResponse.
+type CreateJobResponse struct {
+ // JobId Unique identifier for the created job.
+ JobId openapi_types.UUID `json:"job_id"`
+
+ // Revision The KV revision number.
+ Revision *int64 `json:"revision,omitempty"`
+
+ // Status Initial status of the job.
+ Status string `json:"status"`
+
+ // Timestamp Creation timestamp.
+ Timestamp *string `json:"timestamp,omitempty"`
+}
+
+// DNSConfigCollectionResponse defines model for DNSConfigCollectionResponse.
+type DNSConfigCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []DNSConfigResponse `json:"results"`
+}
+
+// DNSConfigResponse defines model for DNSConfigResponse.
+type DNSConfigResponse struct {
+ // Error Error message if the agent failed.
+ Error *string `json:"error,omitempty"`
+
+ // Hostname The hostname of the agent that served this config.
+ Hostname string `json:"hostname"`
+
+ // SearchDomains List of search domains.
+ SearchDomains *[]string `json:"search_domains,omitempty"`
+
+ // Servers List of configured DNS servers.
+ Servers *[]string `json:"servers,omitempty"`
+}
+
+// DNSConfigUpdateRequest defines model for DNSConfigUpdateRequest.
+type DNSConfigUpdateRequest struct {
+ // InterfaceName The name of the network interface to apply DNS configuration to. Accepts alphanumeric names or @fact. references.
+ InterfaceName string `json:"interface_name" validate:"required,alphanum_or_fact"`
+
+ // SearchDomains New list of search domains to configure.
+ SearchDomains *[]string `json:"search_domains,omitempty" validate:"required_without=Servers,omitempty,dive,hostname,min=1"`
+
+ // Servers New list of DNS servers to configure.
+ Servers *[]string `json:"servers,omitempty" validate:"required_without=SearchDomains,omitempty,dive,ip,min=1"`
+}
+
+// DNSUpdateCollectionResponse defines model for DNSUpdateCollectionResponse.
+type DNSUpdateCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []DNSUpdateResultItem `json:"results"`
+}
+
+// DNSUpdateResultItem defines model for DNSUpdateResultItem.
+type DNSUpdateResultItem struct {
+ // Changed Whether the DNS configuration was actually modified.
+ Changed *bool `json:"changed,omitempty"`
+ Error *string `json:"error,omitempty"`
+ Hostname string `json:"hostname"`
+ Status DNSUpdateResultItemStatus `json:"status"`
+}
+
+// DNSUpdateResultItemStatus defines model for DNSUpdateResultItem.Status.
+type DNSUpdateResultItemStatus string
+
+// DiskCollectionResponse defines model for DiskCollectionResponse.
+type DiskCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []DiskResultItem `json:"results"`
+}
+
+// DiskResponse Local disk usage information.
+type DiskResponse struct {
+ // Free Free disk space in bytes.
+ Free int `json:"free"`
+
+ // Name Disk identifier, e.g., "/dev/sda1".
+ Name string `json:"name"`
+
+ // Total Total disk space in bytes.
+ Total int `json:"total"`
+
+ // Used Used disk space in bytes.
+ Used int `json:"used"`
+}
+
+// DiskResultItem defines model for DiskResultItem.
+type DiskResultItem struct {
+ // Disks List of local disk usage information.
+ Disks *DisksResponse `json:"disks,omitempty"`
+
+ // Error Error message if the agent failed.
+ Error *string `json:"error,omitempty"`
+
+ // Hostname The hostname of the agent.
+ Hostname string `json:"hostname"`
+}
+
+// DisksResponse List of local disk usage information.
+type DisksResponse = []DiskResponse
+
+// ErrorResponse defines model for ErrorResponse.
+type ErrorResponse struct {
+ // Code The error code.
+ Code *int `json:"code,omitempty"`
+
+ // Details Additional details about the error.
+ Details *string `json:"details,omitempty"`
+
+ // Error A description of the error that occurred.
+ Error *string `json:"error,omitempty"`
+}
+
+// FileDeleteResponse defines model for FileDeleteResponse.
+type FileDeleteResponse struct {
+ // Deleted Whether the file was deleted.
+ Deleted bool `json:"deleted"`
+
+ // Name The name of the deleted file.
+ Name string `json:"name"`
+}
+
+// FileDeployRequest defines model for FileDeployRequest.
+type FileDeployRequest struct {
+ // ContentType Content type — "raw" or "template".
+ ContentType FileDeployRequestContentType `json:"content_type" validate:"required,oneof=raw template"`
+
+ // Group File owner group.
+ Group *string `json:"group,omitempty"`
+
+ // Mode File permission mode (e.g., "0644").
+ Mode *string `json:"mode,omitempty"`
+
+ // ObjectName Name of the file in the Object Store.
+ ObjectName string `json:"object_name" validate:"required,min=1,max=255"`
+
+ // Owner File owner user.
+ Owner *string `json:"owner,omitempty"`
+
+ // Path Destination path on the target filesystem.
+ Path string `json:"path" validate:"required,min=1"`
+
+ // Vars Template variables when content_type is "template".
+ Vars *map[string]interface{} `json:"vars,omitempty"`
+}
+
+// FileDeployRequestContentType Content type — "raw" or "template".
+type FileDeployRequestContentType string
+
+// FileDeployResponse defines model for FileDeployResponse.
+type FileDeployResponse struct {
+ // Changed Whether the file was actually written.
+ Changed bool `json:"changed"`
+
+ // Hostname The agent that processed the job.
+ Hostname string `json:"hostname"`
+
+ // JobId The ID of the created job.
+ JobId string `json:"job_id"`
+}
+
+// FileInfo defines model for FileInfo.
+type FileInfo struct {
+ // ContentType How the file should be treated during deploy (raw or template).
+ ContentType string `json:"content_type"`
+
+ // Name The name of the file.
+ Name string `json:"name"`
+
+ // Sha256 SHA-256 hash of the file content.
+ Sha256 string `json:"sha256"`
+
+ // Size File size in bytes.
+ Size int `json:"size"`
+}
+
+// FileInfoResponse defines model for FileInfoResponse.
+type FileInfoResponse struct {
+ // ContentType How the file should be treated during deploy (raw or template).
+ ContentType string `json:"content_type"`
+
+ // Name The name of the file.
+ Name string `json:"name"`
+
+ // Sha256 SHA-256 hash of the file content.
+ Sha256 string `json:"sha256"`
+
+ // Size File size in bytes.
+ Size int `json:"size"`
+}
+
+// FileListResponse defines model for FileListResponse.
+type FileListResponse struct {
+ // Files List of stored files.
+ Files []FileInfo `json:"files"`
+
+ // Total Total number of files.
+ Total int `json:"total"`
+}
+
+// FileStatusRequest defines model for FileStatusRequest.
+type FileStatusRequest struct {
+ // Path Filesystem path to check.
+ Path string `json:"path" validate:"required,min=1"`
+}
+
+// FileStatusResponse defines model for FileStatusResponse.
+type FileStatusResponse struct {
+ // Hostname The agent that processed the job.
+ Hostname string `json:"hostname"`
+
+ // JobId The ID of the created job.
+ JobId string `json:"job_id"`
+
+ // Path The filesystem path.
+ Path string `json:"path"`
+
+ // Sha256 Current SHA-256 of the file on disk.
+ Sha256 *string `json:"sha256,omitempty"`
+
+ // Status File state — "in-sync", "drifted", or "missing".
+ Status string `json:"status"`
+}
+
+// FileUploadResponse defines model for FileUploadResponse.
+type FileUploadResponse struct {
+ // Changed Whether the file content changed. False when the Object Store already held an object with the same SHA-256 digest.
+ Changed bool `json:"changed"`
+
+ // ContentType How the file should be treated during deploy (raw or template).
+ ContentType string `json:"content_type"`
+
+ // Name The name of the uploaded file.
+ Name string `json:"name"`
+
+ // Sha256 SHA-256 hash of the file content.
+ Sha256 string `json:"sha256"`
+
+ // Size File size in bytes.
+ Size int `json:"size"`
+}
+
+// HealthResponse defines model for HealthResponse.
+type HealthResponse struct {
+ // Status Health status.
+ Status string `json:"status"`
+}
+
+// HostnameCollectionResponse defines model for HostnameCollectionResponse.
+type HostnameCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []HostnameResponse `json:"results"`
+}
+
+// HostnameResponse The hostname of the system.
+type HostnameResponse struct {
+ // Error Error message if the agent failed.
+ Error *string `json:"error,omitempty"`
+
+ // Hostname The system's hostname.
+ Hostname string `json:"hostname"`
+
+ // Labels Key-value labels configured on the agent.
+ Labels *map[string]string `json:"labels,omitempty"`
+}
+
+// JobDetailResponse defines model for JobDetailResponse.
+type JobDetailResponse struct {
+ // AgentStates Per-agent processing state for broadcast jobs.
+ AgentStates *map[string]struct {
+ Duration *string `json:"duration,omitempty"`
+ Error *string `json:"error,omitempty"`
+ Status *string `json:"status,omitempty"`
+ } `json:"agent_states,omitempty"`
+
+ // Created Creation timestamp.
+ Created *string `json:"created,omitempty"`
+
+ // Error Error message if failed.
+ Error *string `json:"error,omitempty"`
+
+ // Hostname Agent hostname that processed the job.
+ Hostname *string `json:"hostname,omitempty"`
+
+ // Id Unique identifier of the job.
+ Id *openapi_types.UUID `json:"id,omitempty"`
+
+ // Operation The operation data.
+ Operation *map[string]interface{} `json:"operation,omitempty"`
+
+ // Responses Per-agent response data for broadcast jobs.
+ Responses *map[string]struct {
+ // Data Agent result data.
+ Data interface{} `json:"data,omitempty"`
+ Error *string `json:"error,omitempty"`
+ Hostname *string `json:"hostname,omitempty"`
+ Status *string `json:"status,omitempty"`
+ } `json:"responses,omitempty"`
+
+ // Result The result data if completed.
+ Result interface{} `json:"result,omitempty"`
+
+ // Status Current status of the job.
+ Status *string `json:"status,omitempty"`
+
+ // Timeline Chronological sequence of job lifecycle events.
+ Timeline *[]struct {
+ // Error Error details if applicable.
+ Error *string `json:"error,omitempty"`
+
+ // Event Event type (submitted, acknowledged, started, completed, failed, retried).
+ Event *string `json:"event,omitempty"`
+
+ // Hostname Agent or source that generated the event.
+ Hostname *string `json:"hostname,omitempty"`
+
+ // Message Human-readable description of the event.
+ Message *string `json:"message,omitempty"`
+
+ // Timestamp ISO 8601 timestamp of the event.
+ Timestamp *string `json:"timestamp,omitempty"`
+ } `json:"timeline,omitempty"`
+
+ // UpdatedAt Last update timestamp.
+ UpdatedAt *string `json:"updated_at,omitempty"`
+}
+
+// JobStats defines model for JobStats.
+type JobStats struct {
+ // Completed Number of completed jobs.
+ Completed int `json:"completed"`
+
+ // Dlq Number of jobs in the dead letter queue.
+ Dlq int `json:"dlq"`
+
+ // Failed Number of failed jobs.
+ Failed int `json:"failed"`
+
+ // Processing Number of jobs currently processing.
+ Processing int `json:"processing"`
+
+ // Total Total number of jobs.
+ Total int `json:"total"`
+
+ // Unprocessed Number of unprocessed jobs.
+ Unprocessed int `json:"unprocessed"`
+}
+
+// KVBucketInfo defines model for KVBucketInfo.
+type KVBucketInfo struct {
+ // Bytes Total bytes in the bucket.
+ Bytes int `json:"bytes"`
+
+ // Keys Number of keys in the bucket.
+ Keys int `json:"keys"`
+
+ // Name KV bucket name.
+ Name string `json:"name"`
+}
+
+// ListAgentsResponse defines model for ListAgentsResponse.
+type ListAgentsResponse struct {
+ Agents []AgentInfo `json:"agents"`
+
+ // Total Total number of active agents.
+ Total int `json:"total"`
+}
+
+// ListAuditResponse defines model for ListAuditResponse.
+type ListAuditResponse struct {
+ // Items The audit entries for this page.
+ Items []AuditEntry `json:"items"`
+
+ // TotalItems Total number of audit entries.
+ TotalItems int `json:"total_items"`
+}
+
+// ListJobsResponse defines model for ListJobsResponse.
+type ListJobsResponse struct {
+ Items *[]JobDetailResponse `json:"items,omitempty"`
+
+ // StatusCounts Count of all jobs by status (submitted, processing, completed, failed, partial_failure). Derived from key names during the listing pass — no extra reads.
+ StatusCounts *map[string]int `json:"status_counts,omitempty"`
+
+ // TotalItems Total number of jobs matching the filter.
+ TotalItems *int `json:"total_items,omitempty"`
+}
+
+// LoadAverageResponse The system load averages for 1, 5, and 15 minutes.
+type LoadAverageResponse struct {
+ // N15min Load average for the last 15 minutes.
+ N15min float32 `json:"15min"`
+
+ // N1min Load average for the last 1 minute.
+ N1min float32 `json:"1min"`
+
+ // N5min Load average for the last 5 minutes.
+ N5min float32 `json:"5min"`
+}
+
+// LoadCollectionResponse defines model for LoadCollectionResponse.
+type LoadCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []LoadResultItem `json:"results"`
+}
+
+// LoadResultItem defines model for LoadResultItem.
+type LoadResultItem struct {
+ // Error Error message if the agent failed.
+ Error *string `json:"error,omitempty"`
+
+ // Hostname The hostname of the agent.
+ Hostname string `json:"hostname"`
+
+ // LoadAverage The system load averages for 1, 5, and 15 minutes.
+ LoadAverage *LoadAverageResponse `json:"load_average,omitempty"`
+}
+
+// MemoryCollectionResponse defines model for MemoryCollectionResponse.
+type MemoryCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []MemoryResultItem `json:"results"`
+}
+
+// MemoryResponse Memory usage information.
+type MemoryResponse struct {
+ // Free Free memory in bytes.
+ Free int `json:"free"`
+
+ // Total Total memory in bytes.
+ Total int `json:"total"`
+
+ // Used Used memory in bytes.
+ Used int `json:"used"`
+}
+
+// MemoryResultItem defines model for MemoryResultItem.
+type MemoryResultItem struct {
+ // Error Error message if the agent failed.
+ Error *string `json:"error,omitempty"`
+
+ // Hostname The hostname of the agent.
+ Hostname string `json:"hostname"`
+
+ // Memory Memory usage information.
+ Memory *MemoryResponse `json:"memory,omitempty"`
+}
+
+// NATSInfo defines model for NATSInfo.
+type NATSInfo struct {
+ // Url Connected NATS server URL.
+ Url string `json:"url"`
+
+ // Version NATS server version.
+ 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
+
+// NodeCondition defines model for NodeCondition.
+type NodeCondition struct {
+ LastTransitionTime time.Time `json:"last_transition_time"`
+ Reason *string `json:"reason,omitempty"`
+ Status bool `json:"status"`
+ Type NodeConditionType `json:"type"`
+}
+
+// NodeConditionType defines model for NodeCondition.Type.
+type NodeConditionType string
+
+// NodeStatusCollectionResponse defines model for NodeStatusCollectionResponse.
+type NodeStatusCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []NodeStatusResponse `json:"results"`
+}
+
+// NodeStatusResponse defines model for NodeStatusResponse.
+type NodeStatusResponse struct {
+ // Disks List of local disk usage information.
+ Disks *DisksResponse `json:"disks,omitempty"`
+
+ // Error Error message if the agent failed.
+ Error *string `json:"error,omitempty"`
+
+ // Hostname The hostname of the system.
+ Hostname string `json:"hostname"`
+
+ // LoadAverage The system load averages for 1, 5, and 15 minutes.
+ LoadAverage *LoadAverageResponse `json:"load_average,omitempty"`
+
+ // Memory Memory usage information.
+ Memory *MemoryResponse `json:"memory,omitempty"`
+
+ // OsInfo Operating system information.
+ OsInfo *OSInfoResponse `json:"os_info,omitempty"`
+
+ // Uptime The uptime of the system.
+ Uptime *string `json:"uptime,omitempty"`
+}
+
+// OSInfoCollectionResponse defines model for OSInfoCollectionResponse.
+type OSInfoCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []OSInfoResultItem `json:"results"`
+}
+
+// OSInfoResponse Operating system information.
+type OSInfoResponse struct {
+ // Distribution The name of the Linux distribution.
+ Distribution string `json:"distribution"`
+
+ // Version The version of the Linux distribution.
+ Version string `json:"version"`
+}
+
+// OSInfoResultItem defines model for OSInfoResultItem.
+type OSInfoResultItem struct {
+ // Error Error message if the agent failed.
+ Error *string `json:"error,omitempty"`
+
+ // Hostname The hostname of the agent.
+ Hostname string `json:"hostname"`
+
+ // OsInfo Operating system information.
+ OsInfo *OSInfoResponse `json:"os_info,omitempty"`
+}
+
+// ObjectStoreInfo defines model for ObjectStoreInfo.
+type ObjectStoreInfo struct {
+ // Name Object Store bucket name.
+ Name string `json:"name"`
+
+ // Size Total bytes in the store.
+ Size int `json:"size"`
+}
+
+// PingCollectionResponse defines model for PingCollectionResponse.
+type PingCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []PingResponse `json:"results"`
+}
+
+// PingResponse defines model for PingResponse.
+type PingResponse struct {
+ // AvgRtt Average round-trip time in Go time.Duration format.
+ AvgRtt *string `json:"avg_rtt,omitempty"`
+
+ // Error Error message if the agent failed.
+ Error *string `json:"error,omitempty"`
+
+ // Hostname The hostname of the agent that executed the ping.
+ Hostname string `json:"hostname"`
+
+ // MaxRtt Maximum round-trip time in Go time.Duration format.
+ MaxRtt *string `json:"max_rtt,omitempty"`
+
+ // MinRtt Minimum round-trip time in Go time.Duration format.
+ MinRtt *string `json:"min_rtt,omitempty"`
+
+ // PacketLoss Percentage of packet loss.
+ PacketLoss *float64 `json:"packet_loss,omitempty"`
+
+ // PacketsReceived Number of packets received.
+ PacketsReceived *int `json:"packets_received,omitempty"`
+
+ // PacketsSent Number of packets sent.
+ PacketsSent *int `json:"packets_sent,omitempty"`
+}
+
+// QueueStatsResponse defines model for QueueStatsResponse.
+type QueueStatsResponse struct {
+ // DlqCount Number of jobs in the dead letter queue.
+ DlqCount *int `json:"dlq_count,omitempty"`
+
+ // StatusCounts Count of jobs by status.
+ StatusCounts *map[string]int `json:"status_counts,omitempty"`
+
+ // TotalJobs Total number of jobs in the queue.
+ TotalJobs *int `json:"total_jobs,omitempty"`
+}
+
+// ReadyResponse defines model for ReadyResponse.
+type ReadyResponse struct {
+ // Error Error message when not ready.
+ Error *string `json:"error,omitempty"`
+
+ // Status Readiness status.
+ Status string `json:"status"`
+}
+
+// RetryJobRequest defines model for RetryJobRequest.
+type RetryJobRequest struct {
+ // TargetHostname Override target hostname for the retried job. Defaults to _any if not specified.
+ TargetHostname *string `json:"target_hostname,omitempty" validate:"omitempty,min=1,valid_target"`
+}
+
+// RouteResponse A network routing table entry.
+type RouteResponse struct {
+ // Destination Destination network address.
+ Destination string `json:"destination"`
+
+ // Flags Route flags.
+ Flags *string `json:"flags,omitempty"`
+
+ // Gateway Gateway address.
+ Gateway string `json:"gateway"`
+
+ // Interface Network interface name.
+ Interface string `json:"interface"`
+
+ // Mask Network mask in CIDR notation.
+ Mask *string `json:"mask,omitempty"`
+
+ // Metric Route metric.
+ Metric *int `json:"metric,omitempty"`
+}
+
+// StatusResponse defines model for StatusResponse.
+type StatusResponse struct {
+ Agents *AgentStats `json:"agents,omitempty"`
+
+ // Components Per-component health status.
+ Components map[string]ComponentHealth `json:"components"`
+ Consumers *ConsumerStats `json:"consumers,omitempty"`
+ Jobs *JobStats `json:"jobs,omitempty"`
+
+ // KvBuckets KV bucket statistics.
+ KvBuckets *[]KVBucketInfo `json:"kv_buckets,omitempty"`
+ Nats *NATSInfo `json:"nats,omitempty"`
+
+ // ObjectStores Object Store statistics.
+ ObjectStores *[]ObjectStoreInfo `json:"object_stores,omitempty"`
+
+ // Status Overall health status.
+ Status string `json:"status"`
+
+ // Streams JetStream stream statistics.
+ Streams *[]StreamInfo `json:"streams,omitempty"`
+
+ // Uptime Time since server started.
+ Uptime string `json:"uptime"`
+
+ // Version Application version.
+ Version string `json:"version"`
+}
+
+// StreamInfo defines model for StreamInfo.
+type StreamInfo struct {
+ // Bytes Total bytes in the stream.
+ Bytes int `json:"bytes"`
+
+ // Consumers Number of consumers on the stream.
+ Consumers int `json:"consumers"`
+
+ // Messages Number of messages in the stream.
+ Messages int `json:"messages"`
+
+ // Name Stream name.
+ Name string `json:"name"`
+}
+
+// TimelineEvent defines model for TimelineEvent.
+type TimelineEvent struct {
+ Error *string `json:"error,omitempty"`
+ Event string `json:"event"`
+ Hostname *string `json:"hostname,omitempty"`
+ Message *string `json:"message,omitempty"`
+ Timestamp time.Time `json:"timestamp"`
+}
+
+// UptimeCollectionResponse defines model for UptimeCollectionResponse.
+type UptimeCollectionResponse struct {
+ // JobId The job ID used to process this request.
+ JobId *openapi_types.UUID `json:"job_id,omitempty"`
+ Results []UptimeResponse `json:"results"`
+}
+
+// UptimeResponse System uptime information.
+type UptimeResponse struct {
+ // Error Error message if the agent failed.
+ Error *string `json:"error,omitempty"`
+
+ // Hostname The hostname of the agent.
+ Hostname string `json:"hostname"`
+
+ // Uptime The uptime of the system.
+ Uptime *string `json:"uptime,omitempty"`
+}
+
+// FileName defines model for FileName.
+type FileName = string
+
+// Hostname defines model for Hostname.
+type Hostname = string
+
+// GetAuditLogsParams defines parameters for GetAuditLogs.
+type GetAuditLogsParams struct {
+ // Limit Maximum number of entries to return.
+ Limit *int `form:"limit,omitempty" json:"limit,omitempty" validate:"omitempty,min=1,max=100"`
+
+ // Offset Number of entries to skip.
+ Offset *int `form:"offset,omitempty" json:"offset,omitempty" validate:"omitempty,min=0"`
+}
+
+// PostFileMultipartBody defines parameters for PostFile.
+type PostFileMultipartBody struct {
+ // ContentType How the file should be treated during deploy. "raw" writes bytes as-is; "template" renders with Go text/template and agent facts.
+ ContentType *PostFileMultipartBodyContentType `json:"content_type,omitempty"`
+
+ // File The file content.
+ File openapi_types.File `json:"file"`
+
+ // Name The name of the file in the Object Store.
+ Name string `json:"name"`
+}
+
+// PostFileParams defines parameters for PostFile.
+type PostFileParams struct {
+ // Force When true, bypass the digest check and always write the file. Returns changed=true regardless of whether the content differs from the existing object.
+ Force *bool `form:"force,omitempty" json:"force,omitempty" validate:"omitempty"`
+}
+
+// PostFileMultipartBodyContentType defines parameters for PostFile.
+type PostFileMultipartBodyContentType string
+
+// GetJobParams defines parameters for GetJob.
+type GetJobParams struct {
+ // Status Filter jobs by status.
+ Status *GetJobParamsStatus `form:"status,omitempty" json:"status,omitempty" validate:"omitempty,oneof=submitted processing completed failed partial_failure"`
+
+ // Limit Maximum number of jobs per page (1-100).
+ Limit *int `form:"limit,omitempty" json:"limit,omitempty" validate:"omitempty,min=1,max=100"`
+
+ // Offset Number of jobs to skip for pagination.
+ Offset *int `form:"offset,omitempty" json:"offset,omitempty" validate:"omitempty,min=0"`
+}
+
+// GetJobParamsStatus defines parameters for GetJob.
+type GetJobParamsStatus string
+
+// PostNodeNetworkPingJSONBody defines parameters for PostNodeNetworkPing.
+type PostNodeNetworkPingJSONBody struct {
+ // Address The IP address of the server to ping. Supports both IPv4 and IPv6. Also accepts @fact. references that are resolved agent-side.
+ Address string `json:"address" validate:"required,ip_or_fact"`
+}
+
+// PostFileMultipartRequestBody defines body for PostFile for multipart/form-data ContentType.
+type PostFileMultipartRequestBody PostFileMultipartBody
+
+// PostJobJSONRequestBody defines body for PostJob for application/json ContentType.
+type PostJobJSONRequestBody = CreateJobRequest
+
+// RetryJobByIDJSONRequestBody defines body for RetryJobByID for application/json ContentType.
+type RetryJobByIDJSONRequestBody = RetryJobRequest
+
+// PostNodeCommandExecJSONRequestBody defines body for PostNodeCommandExec for application/json ContentType.
+type PostNodeCommandExecJSONRequestBody = CommandExecRequest
+
+// PostNodeCommandShellJSONRequestBody defines body for PostNodeCommandShell for application/json ContentType.
+type PostNodeCommandShellJSONRequestBody = CommandShellRequest
+
+// PostNodeFileDeployJSONRequestBody defines body for PostNodeFileDeploy for application/json ContentType.
+type PostNodeFileDeployJSONRequestBody = FileDeployRequest
+
+// PostNodeFileStatusJSONRequestBody defines body for PostNodeFileStatus for application/json ContentType.
+type PostNodeFileStatusJSONRequestBody = FileStatusRequest
+
+// PutNodeNetworkDNSJSONRequestBody defines body for PutNodeNetworkDNS for application/json ContentType.
+type PutNodeNetworkDNSJSONRequestBody = DNSConfigUpdateRequest
+
+// PostNodeNetworkPingJSONRequestBody defines body for PostNodeNetworkPing for application/json ContentType.
+type PostNodeNetworkPingJSONRequestBody PostNodeNetworkPingJSONBody
+
+// RequestEditorFn is the function signature for the RequestEditor callback function
+type RequestEditorFn func(ctx context.Context, req *http.Request) error
+
+// Doer performs HTTP requests.
+//
+// The standard http.Client implements this interface.
+type HttpRequestDoer interface {
+ Do(req *http.Request) (*http.Response, error)
+}
+
+// Client which conforms to the OpenAPI3 specification for this service.
+type Client struct {
+ // The endpoint of the server conforming to this interface, with scheme,
+ // https://api.deepmap.com for example. This can contain a path relative
+ // to the server, such as https://api.deepmap.com/dev-test, and all the
+ // paths in the swagger spec will be appended to the server.
+ Server string
+
+ // Doer for performing requests, typically a *http.Client with any
+ // customized settings, such as certificate chains.
+ Client HttpRequestDoer
+
+ // A list of callbacks for modifying requests which are generated before sending over
+ // the network.
+ RequestEditors []RequestEditorFn
+}
+
+// ClientOption allows setting custom parameters during construction
+type ClientOption func(*Client) error
+
+// Creates a new Client, with reasonable defaults
+func NewClient(server string, opts ...ClientOption) (*Client, error) {
+ // create a client with sane default values
+ client := Client{
+ Server: server,
+ }
+ // mutate client and add all optional params
+ for _, o := range opts {
+ if err := o(&client); err != nil {
+ return nil, err
+ }
+ }
+ // ensure the server URL always has a trailing slash
+ if !strings.HasSuffix(client.Server, "/") {
+ client.Server += "/"
+ }
+ // create httpClient, if not already present
+ if client.Client == nil {
+ client.Client = &http.Client{}
+ }
+ return &client, nil
+}
+
+// WithHTTPClient allows overriding the default Doer, which is
+// automatically created using http.Client. This is useful for tests.
+func WithHTTPClient(doer HttpRequestDoer) ClientOption {
+ return func(c *Client) error {
+ c.Client = doer
+ return nil
+ }
+}
+
+// WithRequestEditorFn allows setting up a callback function, which will be
+// called right before sending the request. This can be used to mutate the request.
+func WithRequestEditorFn(fn RequestEditorFn) ClientOption {
+ return func(c *Client) error {
+ c.RequestEditors = append(c.RequestEditors, fn)
+ return nil
+ }
+}
+
+// The interface specification for the client above.
+type ClientInterface interface {
+ // GetAgent request
+ GetAgent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetAgentDetails request
+ GetAgentDetails(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // DrainAgent request
+ DrainAgent(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // UndrainAgent request
+ UndrainAgent(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetAuditLogs request
+ GetAuditLogs(ctx context.Context, params *GetAuditLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetAuditExport request
+ GetAuditExport(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetAuditLogByID request
+ GetAuditLogByID(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetFiles request
+ GetFiles(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // PostFileWithBody request with any body
+ PostFileWithBody(ctx context.Context, params *PostFileParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // DeleteFileByName request
+ DeleteFileByName(ctx context.Context, name FileName, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetFileByName request
+ GetFileByName(ctx context.Context, name FileName, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetHealth request
+ GetHealth(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetHealthReady request
+ GetHealthReady(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetHealthStatus request
+ GetHealthStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetJob request
+ GetJob(ctx context.Context, params *GetJobParams, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // PostJobWithBody request with any body
+ PostJobWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ PostJob(ctx context.Context, body PostJobJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetJobStatus request
+ GetJobStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // DeleteJobByID request
+ DeleteJobByID(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetJobByID request
+ GetJobByID(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // RetryJobByIDWithBody request with any body
+ RetryJobByIDWithBody(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ RetryJobByID(ctx context.Context, id openapi_types.UUID, body RetryJobByIDJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetNodeStatus request
+ GetNodeStatus(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // PostNodeCommandExecWithBody request with any body
+ PostNodeCommandExecWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ PostNodeCommandExec(ctx context.Context, hostname Hostname, body PostNodeCommandExecJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // PostNodeCommandShellWithBody request with any body
+ PostNodeCommandShellWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ PostNodeCommandShell(ctx context.Context, hostname Hostname, body PostNodeCommandShellJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetNodeDisk request
+ GetNodeDisk(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // PostNodeFileDeployWithBody request with any body
+ PostNodeFileDeployWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ PostNodeFileDeploy(ctx context.Context, hostname Hostname, body PostNodeFileDeployJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // PostNodeFileStatusWithBody request with any body
+ PostNodeFileStatusWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ PostNodeFileStatus(ctx context.Context, hostname Hostname, body PostNodeFileStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetNodeHostname request
+ GetNodeHostname(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetNodeLoad request
+ GetNodeLoad(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetNodeMemory request
+ GetNodeMemory(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // PutNodeNetworkDNSWithBody request with any body
+ PutNodeNetworkDNSWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ PutNodeNetworkDNS(ctx context.Context, hostname Hostname, body PutNodeNetworkDNSJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetNodeNetworkDNSByInterface request
+ GetNodeNetworkDNSByInterface(ctx context.Context, hostname Hostname, interfaceName string, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // PostNodeNetworkPingWithBody request with any body
+ PostNodeNetworkPingWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ PostNodeNetworkPing(ctx context.Context, hostname Hostname, body PostNodeNetworkPingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetNodeOS request
+ GetNodeOS(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetNodeUptime request
+ GetNodeUptime(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // GetVersion request
+ GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
+}
+
+func (c *Client) GetAgent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetAgentRequest(c.Server)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetAgentDetails(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetAgentDetailsRequest(c.Server, hostname)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) DrainAgent(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewDrainAgentRequest(c.Server, hostname)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) UndrainAgent(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewUndrainAgentRequest(c.Server, hostname)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetAuditLogs(ctx context.Context, params *GetAuditLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetAuditLogsRequest(c.Server, params)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetAuditExport(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetAuditExportRequest(c.Server)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetAuditLogByID(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetAuditLogByIDRequest(c.Server, id)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetFiles(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetFilesRequest(c.Server)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostFileWithBody(ctx context.Context, params *PostFileParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostFileRequestWithBody(c.Server, params, contentType, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) DeleteFileByName(ctx context.Context, name FileName, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewDeleteFileByNameRequest(c.Server, name)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetFileByName(ctx context.Context, name FileName, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetFileByNameRequest(c.Server, name)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetHealth(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetHealthRequest(c.Server)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetHealthReady(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetHealthReadyRequest(c.Server)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetHealthStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetHealthStatusRequest(c.Server)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetJob(ctx context.Context, params *GetJobParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetJobRequest(c.Server, params)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostJobWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostJobRequestWithBody(c.Server, contentType, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostJob(ctx context.Context, body PostJobJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostJobRequest(c.Server, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetJobStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetJobStatusRequest(c.Server)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) DeleteJobByID(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewDeleteJobByIDRequest(c.Server, id)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetJobByID(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetJobByIDRequest(c.Server, id)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) RetryJobByIDWithBody(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewRetryJobByIDRequestWithBody(c.Server, id, contentType, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) RetryJobByID(ctx context.Context, id openapi_types.UUID, body RetryJobByIDJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewRetryJobByIDRequest(c.Server, id, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetNodeStatus(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetNodeStatusRequest(c.Server, hostname)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostNodeCommandExecWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostNodeCommandExecRequestWithBody(c.Server, hostname, contentType, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostNodeCommandExec(ctx context.Context, hostname Hostname, body PostNodeCommandExecJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostNodeCommandExecRequest(c.Server, hostname, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostNodeCommandShellWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostNodeCommandShellRequestWithBody(c.Server, hostname, contentType, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostNodeCommandShell(ctx context.Context, hostname Hostname, body PostNodeCommandShellJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostNodeCommandShellRequest(c.Server, hostname, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetNodeDisk(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetNodeDiskRequest(c.Server, hostname)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostNodeFileDeployWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostNodeFileDeployRequestWithBody(c.Server, hostname, contentType, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostNodeFileDeploy(ctx context.Context, hostname Hostname, body PostNodeFileDeployJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostNodeFileDeployRequest(c.Server, hostname, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostNodeFileStatusWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostNodeFileStatusRequestWithBody(c.Server, hostname, contentType, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostNodeFileStatus(ctx context.Context, hostname Hostname, body PostNodeFileStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostNodeFileStatusRequest(c.Server, hostname, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetNodeHostname(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetNodeHostnameRequest(c.Server, hostname)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetNodeLoad(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetNodeLoadRequest(c.Server, hostname)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetNodeMemory(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetNodeMemoryRequest(c.Server, hostname)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PutNodeNetworkDNSWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPutNodeNetworkDNSRequestWithBody(c.Server, hostname, contentType, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PutNodeNetworkDNS(ctx context.Context, hostname Hostname, body PutNodeNetworkDNSJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPutNodeNetworkDNSRequest(c.Server, hostname, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetNodeNetworkDNSByInterface(ctx context.Context, hostname Hostname, interfaceName string, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetNodeNetworkDNSByInterfaceRequest(c.Server, hostname, interfaceName)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostNodeNetworkPingWithBody(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostNodeNetworkPingRequestWithBody(c.Server, hostname, contentType, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) PostNodeNetworkPing(ctx context.Context, hostname Hostname, body PostNodeNetworkPingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewPostNodeNetworkPingRequest(c.Server, hostname, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetNodeOS(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetNodeOSRequest(c.Server, hostname)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetNodeUptime(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetNodeUptimeRequest(c.Server, hostname)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewGetVersionRequest(c.Server)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+// NewGetAgentRequest generates requests for GetAgent
+func NewGetAgentRequest(server string) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/agent")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetAgentDetailsRequest generates requests for GetAgentDetails
+func NewGetAgentDetailsRequest(server string, hostname string) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/agent/%s", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewDrainAgentRequest generates requests for DrainAgent
+func NewDrainAgentRequest(server string, hostname string) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/agent/%s/drain", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewUndrainAgentRequest generates requests for UndrainAgent
+func NewUndrainAgentRequest(server string, hostname string) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/agent/%s/undrain", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetAuditLogsRequest generates requests for GetAuditLogs
+func NewGetAuditLogsRequest(server string, params *GetAuditLogsParams) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/audit")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ if params != nil {
+ queryValues := queryURL.Query()
+
+ if params.Limit != nil {
+
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil {
+ return nil, err
+ } else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+ return nil, err
+ } else {
+ for k, v := range parsed {
+ for _, v2 := range v {
+ queryValues.Add(k, v2)
+ }
+ }
+ }
+
+ }
+
+ if params.Offset != nil {
+
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); err != nil {
+ return nil, err
+ } else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+ return nil, err
+ } else {
+ for k, v := range parsed {
+ for _, v2 := range v {
+ queryValues.Add(k, v2)
+ }
+ }
+ }
+
+ }
+
+ queryURL.RawQuery = queryValues.Encode()
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetAuditExportRequest generates requests for GetAuditExport
+func NewGetAuditExportRequest(server string) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/audit/export")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetAuditLogByIDRequest generates requests for GetAuditLogByID
+func NewGetAuditLogByIDRequest(server string, id openapi_types.UUID) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/audit/%s", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetFilesRequest generates requests for GetFiles
+func NewGetFilesRequest(server string) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/file")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewPostFileRequestWithBody generates requests for PostFile with any type of body
+func NewPostFileRequestWithBody(server string, params *PostFileParams, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/file")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ if params != nil {
+ queryValues := queryURL.Query()
+
+ if params.Force != nil {
+
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "force", runtime.ParamLocationQuery, *params.Force); err != nil {
+ return nil, err
+ } else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+ return nil, err
+ } else {
+ for k, v := range parsed {
+ for _, v2 := range v {
+ queryValues.Add(k, v2)
+ }
+ }
+ }
+
+ }
+
+ queryURL.RawQuery = queryValues.Encode()
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
+// NewDeleteFileByNameRequest generates requests for DeleteFileByName
+func NewDeleteFileByNameRequest(server string, name FileName) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "name", runtime.ParamLocationPath, name)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/file/%s", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("DELETE", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetFileByNameRequest generates requests for GetFileByName
+func NewGetFileByNameRequest(server string, name FileName) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "name", runtime.ParamLocationPath, name)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/file/%s", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetHealthRequest generates requests for GetHealth
+func NewGetHealthRequest(server string) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/health")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetHealthReadyRequest generates requests for GetHealthReady
+func NewGetHealthReadyRequest(server string) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/health/ready")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetHealthStatusRequest generates requests for GetHealthStatus
+func NewGetHealthStatusRequest(server string) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/health/status")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetJobRequest generates requests for GetJob
+func NewGetJobRequest(server string, params *GetJobParams) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/job")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ if params != nil {
+ queryValues := queryURL.Query()
+
+ if params.Status != nil {
+
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "status", runtime.ParamLocationQuery, *params.Status); err != nil {
+ return nil, err
+ } else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+ return nil, err
+ } else {
+ for k, v := range parsed {
+ for _, v2 := range v {
+ queryValues.Add(k, v2)
+ }
+ }
+ }
+
+ }
+
+ if params.Limit != nil {
+
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil {
+ return nil, err
+ } else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+ return nil, err
+ } else {
+ for k, v := range parsed {
+ for _, v2 := range v {
+ queryValues.Add(k, v2)
+ }
+ }
+ }
+
+ }
+
+ if params.Offset != nil {
+
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); err != nil {
+ return nil, err
+ } else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+ return nil, err
+ } else {
+ for k, v := range parsed {
+ for _, v2 := range v {
+ queryValues.Add(k, v2)
+ }
+ }
+ }
+
+ }
+
+ queryURL.RawQuery = queryValues.Encode()
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewPostJobRequest calls the generic PostJob builder with application/json body
+func NewPostJobRequest(server string, body PostJobJSONRequestBody) (*http.Request, error) {
+ var bodyReader io.Reader
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader = bytes.NewReader(buf)
+ return NewPostJobRequestWithBody(server, "application/json", bodyReader)
+}
+
+// NewPostJobRequestWithBody generates requests for PostJob with any type of body
+func NewPostJobRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/job")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
+// NewGetJobStatusRequest generates requests for GetJobStatus
+func NewGetJobStatusRequest(server string) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/job/status")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewDeleteJobByIDRequest generates requests for DeleteJobByID
+func NewDeleteJobByIDRequest(server string, id openapi_types.UUID) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/job/%s", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("DELETE", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetJobByIDRequest generates requests for GetJobByID
+func NewGetJobByIDRequest(server string, id openapi_types.UUID) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/job/%s", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewRetryJobByIDRequest calls the generic RetryJobByID builder with application/json body
+func NewRetryJobByIDRequest(server string, id openapi_types.UUID, body RetryJobByIDJSONRequestBody) (*http.Request, error) {
+ var bodyReader io.Reader
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader = bytes.NewReader(buf)
+ return NewRetryJobByIDRequestWithBody(server, id, "application/json", bodyReader)
+}
+
+// NewRetryJobByIDRequestWithBody generates requests for RetryJobByID with any type of body
+func NewRetryJobByIDRequestWithBody(server string, id openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/job/%s/retry", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
+// NewGetNodeStatusRequest generates requests for GetNodeStatus
+func NewGetNodeStatusRequest(server string, hostname Hostname) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewPostNodeCommandExecRequest calls the generic PostNodeCommandExec builder with application/json body
+func NewPostNodeCommandExecRequest(server string, hostname Hostname, body PostNodeCommandExecJSONRequestBody) (*http.Request, error) {
+ var bodyReader io.Reader
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader = bytes.NewReader(buf)
+ return NewPostNodeCommandExecRequestWithBody(server, hostname, "application/json", bodyReader)
+}
+
+// NewPostNodeCommandExecRequestWithBody generates requests for PostNodeCommandExec with any type of body
+func NewPostNodeCommandExecRequestWithBody(server string, hostname Hostname, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/command/exec", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
+// NewPostNodeCommandShellRequest calls the generic PostNodeCommandShell builder with application/json body
+func NewPostNodeCommandShellRequest(server string, hostname Hostname, body PostNodeCommandShellJSONRequestBody) (*http.Request, error) {
+ var bodyReader io.Reader
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader = bytes.NewReader(buf)
+ return NewPostNodeCommandShellRequestWithBody(server, hostname, "application/json", bodyReader)
+}
+
+// NewPostNodeCommandShellRequestWithBody generates requests for PostNodeCommandShell with any type of body
+func NewPostNodeCommandShellRequestWithBody(server string, hostname Hostname, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/command/shell", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
+// NewGetNodeDiskRequest generates requests for GetNodeDisk
+func NewGetNodeDiskRequest(server string, hostname Hostname) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/disk", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewPostNodeFileDeployRequest calls the generic PostNodeFileDeploy builder with application/json body
+func NewPostNodeFileDeployRequest(server string, hostname Hostname, body PostNodeFileDeployJSONRequestBody) (*http.Request, error) {
+ var bodyReader io.Reader
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader = bytes.NewReader(buf)
+ return NewPostNodeFileDeployRequestWithBody(server, hostname, "application/json", bodyReader)
+}
+
+// NewPostNodeFileDeployRequestWithBody generates requests for PostNodeFileDeploy with any type of body
+func NewPostNodeFileDeployRequestWithBody(server string, hostname Hostname, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/file/deploy", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
+// NewPostNodeFileStatusRequest calls the generic PostNodeFileStatus builder with application/json body
+func NewPostNodeFileStatusRequest(server string, hostname Hostname, body PostNodeFileStatusJSONRequestBody) (*http.Request, error) {
+ var bodyReader io.Reader
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader = bytes.NewReader(buf)
+ return NewPostNodeFileStatusRequestWithBody(server, hostname, "application/json", bodyReader)
+}
+
+// NewPostNodeFileStatusRequestWithBody generates requests for PostNodeFileStatus with any type of body
+func NewPostNodeFileStatusRequestWithBody(server string, hostname Hostname, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/file/status", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
+// NewGetNodeHostnameRequest generates requests for GetNodeHostname
+func NewGetNodeHostnameRequest(server string, hostname Hostname) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/hostname", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetNodeLoadRequest generates requests for GetNodeLoad
+func NewGetNodeLoadRequest(server string, hostname Hostname) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/load", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetNodeMemoryRequest generates requests for GetNodeMemory
+func NewGetNodeMemoryRequest(server string, hostname Hostname) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/memory", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewPutNodeNetworkDNSRequest calls the generic PutNodeNetworkDNS builder with application/json body
+func NewPutNodeNetworkDNSRequest(server string, hostname Hostname, body PutNodeNetworkDNSJSONRequestBody) (*http.Request, error) {
+ var bodyReader io.Reader
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader = bytes.NewReader(buf)
+ return NewPutNodeNetworkDNSRequestWithBody(server, hostname, "application/json", bodyReader)
+}
+
+// NewPutNodeNetworkDNSRequestWithBody generates requests for PutNodeNetworkDNS with any type of body
+func NewPutNodeNetworkDNSRequestWithBody(server string, hostname Hostname, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/network/dns", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("PUT", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
+// NewGetNodeNetworkDNSByInterfaceRequest generates requests for GetNodeNetworkDNSByInterface
+func NewGetNodeNetworkDNSByInterfaceRequest(server string, hostname Hostname, interfaceName string) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ var pathParam1 string
+
+ pathParam1, err = runtime.StyleParamWithLocation("simple", false, "interfaceName", runtime.ParamLocationPath, interfaceName)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/network/dns/%s", pathParam0, pathParam1)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewPostNodeNetworkPingRequest calls the generic PostNodeNetworkPing builder with application/json body
+func NewPostNodeNetworkPingRequest(server string, hostname Hostname, body PostNodeNetworkPingJSONRequestBody) (*http.Request, error) {
+ var bodyReader io.Reader
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader = bytes.NewReader(buf)
+ return NewPostNodeNetworkPingRequestWithBody(server, hostname, "application/json", bodyReader)
+}
+
+// NewPostNodeNetworkPingRequestWithBody generates requests for PostNodeNetworkPing with any type of body
+func NewPostNodeNetworkPingRequestWithBody(server string, hostname Hostname, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/network/ping", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
+// NewGetNodeOSRequest generates requests for GetNodeOS
+func NewGetNodeOSRequest(server string, hostname Hostname) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/os", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetNodeUptimeRequest generates requests for GetNodeUptime
+func NewGetNodeUptimeRequest(server string, hostname Hostname) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "hostname", runtime.ParamLocationPath, hostname)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/node/%s/uptime", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewGetVersionRequest generates requests for GetVersion
+func NewGetVersionRequest(server string) (*http.Request, error) {
+ var err error
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/version")
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
+ for _, r := range c.RequestEditors {
+ if err := r(ctx, req); err != nil {
+ return err
+ }
+ }
+ for _, r := range additionalEditors {
+ if err := r(ctx, req); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// ClientWithResponses builds on ClientInterface to offer response payloads
+type ClientWithResponses struct {
+ ClientInterface
+}
+
+// NewClientWithResponses creates a new ClientWithResponses, which wraps
+// Client with return type handling
+func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) {
+ client, err := NewClient(server, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return &ClientWithResponses{client}, nil
+}
+
+// WithBaseURL overrides the baseURL.
+func WithBaseURL(baseURL string) ClientOption {
+ return func(c *Client) error {
+ newBaseURL, err := url.Parse(baseURL)
+ if err != nil {
+ return err
+ }
+ c.Server = newBaseURL.String()
+ return nil
+ }
+}
+
+// ClientWithResponsesInterface is the interface specification for the client with responses above.
+type ClientWithResponsesInterface interface {
+ // GetAgentWithResponse request
+ GetAgentWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetAgentResponse, error)
+
+ // GetAgentDetailsWithResponse request
+ GetAgentDetailsWithResponse(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*GetAgentDetailsResponse, error)
+
+ // DrainAgentWithResponse request
+ DrainAgentWithResponse(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*DrainAgentResponse, error)
+
+ // UndrainAgentWithResponse request
+ UndrainAgentWithResponse(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*UndrainAgentResponse, error)
+
+ // GetAuditLogsWithResponse request
+ GetAuditLogsWithResponse(ctx context.Context, params *GetAuditLogsParams, reqEditors ...RequestEditorFn) (*GetAuditLogsResponse, error)
+
+ // GetAuditExportWithResponse request
+ GetAuditExportWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetAuditExportResponse, error)
+
+ // GetAuditLogByIDWithResponse request
+ GetAuditLogByIDWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetAuditLogByIDResponse, error)
+
+ // GetFilesWithResponse request
+ GetFilesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetFilesResponse, error)
+
+ // PostFileWithBodyWithResponse request with any body
+ PostFileWithBodyWithResponse(ctx context.Context, params *PostFileParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostFileResponse, error)
+
+ // DeleteFileByNameWithResponse request
+ DeleteFileByNameWithResponse(ctx context.Context, name FileName, reqEditors ...RequestEditorFn) (*DeleteFileByNameResponse, error)
+
+ // GetFileByNameWithResponse request
+ GetFileByNameWithResponse(ctx context.Context, name FileName, reqEditors ...RequestEditorFn) (*GetFileByNameResponse, error)
+
+ // GetHealthWithResponse request
+ GetHealthWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetHealthResponse, error)
+
+ // GetHealthReadyWithResponse request
+ GetHealthReadyWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetHealthReadyResponse, error)
+
+ // GetHealthStatusWithResponse request
+ GetHealthStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetHealthStatusResponse, error)
+
+ // GetJobWithResponse request
+ GetJobWithResponse(ctx context.Context, params *GetJobParams, reqEditors ...RequestEditorFn) (*GetJobResponse, error)
+
+ // PostJobWithBodyWithResponse request with any body
+ PostJobWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostJobResponse, error)
+
+ PostJobWithResponse(ctx context.Context, body PostJobJSONRequestBody, reqEditors ...RequestEditorFn) (*PostJobResponse, error)
+
+ // GetJobStatusWithResponse request
+ GetJobStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetJobStatusResponse, error)
+
+ // DeleteJobByIDWithResponse request
+ DeleteJobByIDWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteJobByIDResponse, error)
+
+ // GetJobByIDWithResponse request
+ GetJobByIDWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetJobByIDResponse, error)
+
+ // RetryJobByIDWithBodyWithResponse request with any body
+ RetryJobByIDWithBodyWithResponse(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RetryJobByIDResponse, error)
+
+ RetryJobByIDWithResponse(ctx context.Context, id openapi_types.UUID, body RetryJobByIDJSONRequestBody, reqEditors ...RequestEditorFn) (*RetryJobByIDResponse, error)
+
+ // GetNodeStatusWithResponse request
+ GetNodeStatusWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeStatusResponse, error)
+
+ // PostNodeCommandExecWithBodyWithResponse request with any body
+ PostNodeCommandExecWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeCommandExecResponse, error)
+
+ PostNodeCommandExecWithResponse(ctx context.Context, hostname Hostname, body PostNodeCommandExecJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeCommandExecResponse, error)
+
+ // PostNodeCommandShellWithBodyWithResponse request with any body
+ PostNodeCommandShellWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeCommandShellResponse, error)
+
+ PostNodeCommandShellWithResponse(ctx context.Context, hostname Hostname, body PostNodeCommandShellJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeCommandShellResponse, error)
+
+ // GetNodeDiskWithResponse request
+ GetNodeDiskWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeDiskResponse, error)
+
+ // PostNodeFileDeployWithBodyWithResponse request with any body
+ PostNodeFileDeployWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeFileDeployResponse, error)
+
+ PostNodeFileDeployWithResponse(ctx context.Context, hostname Hostname, body PostNodeFileDeployJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeFileDeployResponse, error)
+
+ // PostNodeFileStatusWithBodyWithResponse request with any body
+ PostNodeFileStatusWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeFileStatusResponse, error)
+
+ PostNodeFileStatusWithResponse(ctx context.Context, hostname Hostname, body PostNodeFileStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeFileStatusResponse, error)
+
+ // GetNodeHostnameWithResponse request
+ GetNodeHostnameWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeHostnameResponse, error)
+
+ // GetNodeLoadWithResponse request
+ GetNodeLoadWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeLoadResponse, error)
+
+ // GetNodeMemoryWithResponse request
+ GetNodeMemoryWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeMemoryResponse, error)
+
+ // PutNodeNetworkDNSWithBodyWithResponse request with any body
+ PutNodeNetworkDNSWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutNodeNetworkDNSResponse, error)
+
+ PutNodeNetworkDNSWithResponse(ctx context.Context, hostname Hostname, body PutNodeNetworkDNSJSONRequestBody, reqEditors ...RequestEditorFn) (*PutNodeNetworkDNSResponse, error)
+
+ // GetNodeNetworkDNSByInterfaceWithResponse request
+ GetNodeNetworkDNSByInterfaceWithResponse(ctx context.Context, hostname Hostname, interfaceName string, reqEditors ...RequestEditorFn) (*GetNodeNetworkDNSByInterfaceResponse, error)
+
+ // PostNodeNetworkPingWithBodyWithResponse request with any body
+ PostNodeNetworkPingWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeNetworkPingResponse, error)
+
+ PostNodeNetworkPingWithResponse(ctx context.Context, hostname Hostname, body PostNodeNetworkPingJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeNetworkPingResponse, error)
+
+ // GetNodeOSWithResponse request
+ GetNodeOSWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeOSResponse, error)
+
+ // GetNodeUptimeWithResponse request
+ GetNodeUptimeWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeUptimeResponse, error)
+
+ // GetVersionWithResponse request
+ GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error)
+}
+
+type GetAgentResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *ListAgentsResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetAgentResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetAgentResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetAgentDetailsResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *AgentInfo
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON404 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetAgentDetailsResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetAgentDetailsResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type DrainAgentResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *struct {
+ Message string `json:"message"`
+ }
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON404 *ErrorResponse
+ JSON409 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r DrainAgentResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r DrainAgentResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type UndrainAgentResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *struct {
+ Message string `json:"message"`
+ }
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON404 *ErrorResponse
+ JSON409 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r UndrainAgentResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r UndrainAgentResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetAuditLogsResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *ListAuditResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetAuditLogsResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetAuditLogsResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetAuditExportResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *ListAuditResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetAuditExportResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetAuditExportResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetAuditLogByIDResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *AuditEntryResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON404 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetAuditLogByIDResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetAuditLogByIDResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetFilesResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *FileListResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetFilesResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetFilesResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type PostFileResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON201 *FileUploadResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON409 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r PostFileResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r PostFileResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type DeleteFileByNameResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *FileDeleteResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON404 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r DeleteFileByNameResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r DeleteFileByNameResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetFileByNameResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *FileInfoResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON404 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetFileByNameResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetFileByNameResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetHealthResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *HealthResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetHealthResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetHealthResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetHealthReadyResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *ReadyResponse
+ JSON503 *ReadyResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetHealthReadyResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetHealthReadyResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetHealthStatusResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *StatusResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON503 *StatusResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetHealthStatusResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetHealthStatusResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetJobResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *ListJobsResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetJobResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetJobResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type PostJobResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON201 *CreateJobResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r PostJobResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r PostJobResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetJobStatusResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *QueueStatsResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetJobStatusResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetJobStatusResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type DeleteJobByIDResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON404 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r DeleteJobByIDResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r DeleteJobByIDResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetJobByIDResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *JobDetailResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON404 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetJobByIDResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetJobByIDResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type RetryJobByIDResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON201 *CreateJobResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON404 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r RetryJobByIDResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r RetryJobByIDResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetNodeStatusResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *NodeStatusCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetNodeStatusResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetNodeStatusResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type PostNodeCommandExecResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON202 *CommandResultCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r PostNodeCommandExecResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r PostNodeCommandExecResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type PostNodeCommandShellResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON202 *CommandResultCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r PostNodeCommandShellResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r PostNodeCommandShellResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetNodeDiskResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *DiskCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetNodeDiskResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetNodeDiskResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type PostNodeFileDeployResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON202 *FileDeployResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r PostNodeFileDeployResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r PostNodeFileDeployResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type PostNodeFileStatusResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *FileStatusResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r PostNodeFileStatusResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r PostNodeFileStatusResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetNodeHostnameResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *HostnameCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetNodeHostnameResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetNodeHostnameResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetNodeLoadResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *LoadCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetNodeLoadResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetNodeLoadResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetNodeMemoryResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *MemoryCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetNodeMemoryResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetNodeMemoryResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type PutNodeNetworkDNSResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON202 *DNSUpdateCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r PutNodeNetworkDNSResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r PutNodeNetworkDNSResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetNodeNetworkDNSByInterfaceResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *DNSConfigCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetNodeNetworkDNSByInterfaceResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetNodeNetworkDNSByInterfaceResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type PostNodeNetworkPingResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *PingCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r PostNodeNetworkPingResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r PostNodeNetworkPingResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetNodeOSResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *OSInfoCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetNodeOSResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetNodeOSResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetNodeUptimeResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *UptimeCollectionResponse
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON403 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetNodeUptimeResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetNodeUptimeResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type GetVersionResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON400 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r GetVersionResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r GetVersionResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+// GetAgentWithResponse request returning *GetAgentResponse
+func (c *ClientWithResponses) GetAgentWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetAgentResponse, error) {
+ rsp, err := c.GetAgent(ctx, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetAgentResponse(rsp)
+}
+
+// GetAgentDetailsWithResponse request returning *GetAgentDetailsResponse
+func (c *ClientWithResponses) GetAgentDetailsWithResponse(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*GetAgentDetailsResponse, error) {
+ rsp, err := c.GetAgentDetails(ctx, hostname, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetAgentDetailsResponse(rsp)
+}
+
+// DrainAgentWithResponse request returning *DrainAgentResponse
+func (c *ClientWithResponses) DrainAgentWithResponse(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*DrainAgentResponse, error) {
+ rsp, err := c.DrainAgent(ctx, hostname, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseDrainAgentResponse(rsp)
+}
+
+// UndrainAgentWithResponse request returning *UndrainAgentResponse
+func (c *ClientWithResponses) UndrainAgentWithResponse(ctx context.Context, hostname string, reqEditors ...RequestEditorFn) (*UndrainAgentResponse, error) {
+ rsp, err := c.UndrainAgent(ctx, hostname, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseUndrainAgentResponse(rsp)
+}
+
+// GetAuditLogsWithResponse request returning *GetAuditLogsResponse
+func (c *ClientWithResponses) GetAuditLogsWithResponse(ctx context.Context, params *GetAuditLogsParams, reqEditors ...RequestEditorFn) (*GetAuditLogsResponse, error) {
+ rsp, err := c.GetAuditLogs(ctx, params, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetAuditLogsResponse(rsp)
+}
+
+// GetAuditExportWithResponse request returning *GetAuditExportResponse
+func (c *ClientWithResponses) GetAuditExportWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetAuditExportResponse, error) {
+ rsp, err := c.GetAuditExport(ctx, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetAuditExportResponse(rsp)
+}
+
+// GetAuditLogByIDWithResponse request returning *GetAuditLogByIDResponse
+func (c *ClientWithResponses) GetAuditLogByIDWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetAuditLogByIDResponse, error) {
+ rsp, err := c.GetAuditLogByID(ctx, id, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetAuditLogByIDResponse(rsp)
+}
+
+// GetFilesWithResponse request returning *GetFilesResponse
+func (c *ClientWithResponses) GetFilesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetFilesResponse, error) {
+ rsp, err := c.GetFiles(ctx, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetFilesResponse(rsp)
+}
+
+// PostFileWithBodyWithResponse request with arbitrary body returning *PostFileResponse
+func (c *ClientWithResponses) PostFileWithBodyWithResponse(ctx context.Context, params *PostFileParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostFileResponse, error) {
+ rsp, err := c.PostFileWithBody(ctx, params, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostFileResponse(rsp)
+}
+
+// DeleteFileByNameWithResponse request returning *DeleteFileByNameResponse
+func (c *ClientWithResponses) DeleteFileByNameWithResponse(ctx context.Context, name FileName, reqEditors ...RequestEditorFn) (*DeleteFileByNameResponse, error) {
+ rsp, err := c.DeleteFileByName(ctx, name, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseDeleteFileByNameResponse(rsp)
+}
+
+// GetFileByNameWithResponse request returning *GetFileByNameResponse
+func (c *ClientWithResponses) GetFileByNameWithResponse(ctx context.Context, name FileName, reqEditors ...RequestEditorFn) (*GetFileByNameResponse, error) {
+ rsp, err := c.GetFileByName(ctx, name, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetFileByNameResponse(rsp)
+}
+
+// GetHealthWithResponse request returning *GetHealthResponse
+func (c *ClientWithResponses) GetHealthWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetHealthResponse, error) {
+ rsp, err := c.GetHealth(ctx, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetHealthResponse(rsp)
+}
+
+// GetHealthReadyWithResponse request returning *GetHealthReadyResponse
+func (c *ClientWithResponses) GetHealthReadyWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetHealthReadyResponse, error) {
+ rsp, err := c.GetHealthReady(ctx, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetHealthReadyResponse(rsp)
+}
+
+// GetHealthStatusWithResponse request returning *GetHealthStatusResponse
+func (c *ClientWithResponses) GetHealthStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetHealthStatusResponse, error) {
+ rsp, err := c.GetHealthStatus(ctx, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetHealthStatusResponse(rsp)
+}
+
+// GetJobWithResponse request returning *GetJobResponse
+func (c *ClientWithResponses) GetJobWithResponse(ctx context.Context, params *GetJobParams, reqEditors ...RequestEditorFn) (*GetJobResponse, error) {
+ rsp, err := c.GetJob(ctx, params, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetJobResponse(rsp)
+}
+
+// PostJobWithBodyWithResponse request with arbitrary body returning *PostJobResponse
+func (c *ClientWithResponses) PostJobWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostJobResponse, error) {
+ rsp, err := c.PostJobWithBody(ctx, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostJobResponse(rsp)
+}
+
+func (c *ClientWithResponses) PostJobWithResponse(ctx context.Context, body PostJobJSONRequestBody, reqEditors ...RequestEditorFn) (*PostJobResponse, error) {
+ rsp, err := c.PostJob(ctx, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostJobResponse(rsp)
+}
+
+// GetJobStatusWithResponse request returning *GetJobStatusResponse
+func (c *ClientWithResponses) GetJobStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetJobStatusResponse, error) {
+ rsp, err := c.GetJobStatus(ctx, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetJobStatusResponse(rsp)
+}
+
+// DeleteJobByIDWithResponse request returning *DeleteJobByIDResponse
+func (c *ClientWithResponses) DeleteJobByIDWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteJobByIDResponse, error) {
+ rsp, err := c.DeleteJobByID(ctx, id, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseDeleteJobByIDResponse(rsp)
+}
+
+// GetJobByIDWithResponse request returning *GetJobByIDResponse
+func (c *ClientWithResponses) GetJobByIDWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetJobByIDResponse, error) {
+ rsp, err := c.GetJobByID(ctx, id, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetJobByIDResponse(rsp)
+}
+
+// RetryJobByIDWithBodyWithResponse request with arbitrary body returning *RetryJobByIDResponse
+func (c *ClientWithResponses) RetryJobByIDWithBodyWithResponse(ctx context.Context, id openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RetryJobByIDResponse, error) {
+ rsp, err := c.RetryJobByIDWithBody(ctx, id, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseRetryJobByIDResponse(rsp)
+}
+
+func (c *ClientWithResponses) RetryJobByIDWithResponse(ctx context.Context, id openapi_types.UUID, body RetryJobByIDJSONRequestBody, reqEditors ...RequestEditorFn) (*RetryJobByIDResponse, error) {
+ rsp, err := c.RetryJobByID(ctx, id, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseRetryJobByIDResponse(rsp)
+}
+
+// GetNodeStatusWithResponse request returning *GetNodeStatusResponse
+func (c *ClientWithResponses) GetNodeStatusWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeStatusResponse, error) {
+ rsp, err := c.GetNodeStatus(ctx, hostname, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetNodeStatusResponse(rsp)
+}
+
+// PostNodeCommandExecWithBodyWithResponse request with arbitrary body returning *PostNodeCommandExecResponse
+func (c *ClientWithResponses) PostNodeCommandExecWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeCommandExecResponse, error) {
+ rsp, err := c.PostNodeCommandExecWithBody(ctx, hostname, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostNodeCommandExecResponse(rsp)
+}
+
+func (c *ClientWithResponses) PostNodeCommandExecWithResponse(ctx context.Context, hostname Hostname, body PostNodeCommandExecJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeCommandExecResponse, error) {
+ rsp, err := c.PostNodeCommandExec(ctx, hostname, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostNodeCommandExecResponse(rsp)
+}
+
+// PostNodeCommandShellWithBodyWithResponse request with arbitrary body returning *PostNodeCommandShellResponse
+func (c *ClientWithResponses) PostNodeCommandShellWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeCommandShellResponse, error) {
+ rsp, err := c.PostNodeCommandShellWithBody(ctx, hostname, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostNodeCommandShellResponse(rsp)
+}
+
+func (c *ClientWithResponses) PostNodeCommandShellWithResponse(ctx context.Context, hostname Hostname, body PostNodeCommandShellJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeCommandShellResponse, error) {
+ rsp, err := c.PostNodeCommandShell(ctx, hostname, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostNodeCommandShellResponse(rsp)
+}
+
+// GetNodeDiskWithResponse request returning *GetNodeDiskResponse
+func (c *ClientWithResponses) GetNodeDiskWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeDiskResponse, error) {
+ rsp, err := c.GetNodeDisk(ctx, hostname, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetNodeDiskResponse(rsp)
+}
+
+// PostNodeFileDeployWithBodyWithResponse request with arbitrary body returning *PostNodeFileDeployResponse
+func (c *ClientWithResponses) PostNodeFileDeployWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeFileDeployResponse, error) {
+ rsp, err := c.PostNodeFileDeployWithBody(ctx, hostname, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostNodeFileDeployResponse(rsp)
+}
+
+func (c *ClientWithResponses) PostNodeFileDeployWithResponse(ctx context.Context, hostname Hostname, body PostNodeFileDeployJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeFileDeployResponse, error) {
+ rsp, err := c.PostNodeFileDeploy(ctx, hostname, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostNodeFileDeployResponse(rsp)
+}
+
+// PostNodeFileStatusWithBodyWithResponse request with arbitrary body returning *PostNodeFileStatusResponse
+func (c *ClientWithResponses) PostNodeFileStatusWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeFileStatusResponse, error) {
+ rsp, err := c.PostNodeFileStatusWithBody(ctx, hostname, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostNodeFileStatusResponse(rsp)
+}
+
+func (c *ClientWithResponses) PostNodeFileStatusWithResponse(ctx context.Context, hostname Hostname, body PostNodeFileStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeFileStatusResponse, error) {
+ rsp, err := c.PostNodeFileStatus(ctx, hostname, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostNodeFileStatusResponse(rsp)
+}
+
+// GetNodeHostnameWithResponse request returning *GetNodeHostnameResponse
+func (c *ClientWithResponses) GetNodeHostnameWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeHostnameResponse, error) {
+ rsp, err := c.GetNodeHostname(ctx, hostname, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetNodeHostnameResponse(rsp)
+}
+
+// GetNodeLoadWithResponse request returning *GetNodeLoadResponse
+func (c *ClientWithResponses) GetNodeLoadWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeLoadResponse, error) {
+ rsp, err := c.GetNodeLoad(ctx, hostname, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetNodeLoadResponse(rsp)
+}
+
+// GetNodeMemoryWithResponse request returning *GetNodeMemoryResponse
+func (c *ClientWithResponses) GetNodeMemoryWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeMemoryResponse, error) {
+ rsp, err := c.GetNodeMemory(ctx, hostname, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetNodeMemoryResponse(rsp)
+}
+
+// PutNodeNetworkDNSWithBodyWithResponse request with arbitrary body returning *PutNodeNetworkDNSResponse
+func (c *ClientWithResponses) PutNodeNetworkDNSWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutNodeNetworkDNSResponse, error) {
+ rsp, err := c.PutNodeNetworkDNSWithBody(ctx, hostname, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePutNodeNetworkDNSResponse(rsp)
+}
+
+func (c *ClientWithResponses) PutNodeNetworkDNSWithResponse(ctx context.Context, hostname Hostname, body PutNodeNetworkDNSJSONRequestBody, reqEditors ...RequestEditorFn) (*PutNodeNetworkDNSResponse, error) {
+ rsp, err := c.PutNodeNetworkDNS(ctx, hostname, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePutNodeNetworkDNSResponse(rsp)
+}
+
+// GetNodeNetworkDNSByInterfaceWithResponse request returning *GetNodeNetworkDNSByInterfaceResponse
+func (c *ClientWithResponses) GetNodeNetworkDNSByInterfaceWithResponse(ctx context.Context, hostname Hostname, interfaceName string, reqEditors ...RequestEditorFn) (*GetNodeNetworkDNSByInterfaceResponse, error) {
+ rsp, err := c.GetNodeNetworkDNSByInterface(ctx, hostname, interfaceName, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetNodeNetworkDNSByInterfaceResponse(rsp)
+}
+
+// PostNodeNetworkPingWithBodyWithResponse request with arbitrary body returning *PostNodeNetworkPingResponse
+func (c *ClientWithResponses) PostNodeNetworkPingWithBodyWithResponse(ctx context.Context, hostname Hostname, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostNodeNetworkPingResponse, error) {
+ rsp, err := c.PostNodeNetworkPingWithBody(ctx, hostname, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostNodeNetworkPingResponse(rsp)
+}
+
+func (c *ClientWithResponses) PostNodeNetworkPingWithResponse(ctx context.Context, hostname Hostname, body PostNodeNetworkPingJSONRequestBody, reqEditors ...RequestEditorFn) (*PostNodeNetworkPingResponse, error) {
+ rsp, err := c.PostNodeNetworkPing(ctx, hostname, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParsePostNodeNetworkPingResponse(rsp)
+}
+
+// GetNodeOSWithResponse request returning *GetNodeOSResponse
+func (c *ClientWithResponses) GetNodeOSWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeOSResponse, error) {
+ rsp, err := c.GetNodeOS(ctx, hostname, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetNodeOSResponse(rsp)
+}
+
+// GetNodeUptimeWithResponse request returning *GetNodeUptimeResponse
+func (c *ClientWithResponses) GetNodeUptimeWithResponse(ctx context.Context, hostname Hostname, reqEditors ...RequestEditorFn) (*GetNodeUptimeResponse, error) {
+ rsp, err := c.GetNodeUptime(ctx, hostname, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetNodeUptimeResponse(rsp)
+}
+
+// GetVersionWithResponse request returning *GetVersionResponse
+func (c *ClientWithResponses) GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error) {
+ rsp, err := c.GetVersion(ctx, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseGetVersionResponse(rsp)
+}
+
+// ParseGetAgentResponse parses an HTTP response from a GetAgentWithResponse call
+func ParseGetAgentResponse(rsp *http.Response) (*GetAgentResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetAgentResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest ListAgentsResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetAgentDetailsResponse parses an HTTP response from a GetAgentDetailsWithResponse call
+func ParseGetAgentDetailsResponse(rsp *http.Response) (*GetAgentDetailsResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetAgentDetailsResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest AgentInfo
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON404 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseDrainAgentResponse parses an HTTP response from a DrainAgentWithResponse call
+func ParseDrainAgentResponse(rsp *http.Response) (*DrainAgentResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &DrainAgentResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest struct {
+ Message string `json:"message"`
+ }
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON404 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON409 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseUndrainAgentResponse parses an HTTP response from a UndrainAgentWithResponse call
+func ParseUndrainAgentResponse(rsp *http.Response) (*UndrainAgentResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &UndrainAgentResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest struct {
+ Message string `json:"message"`
+ }
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON404 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON409 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetAuditLogsResponse parses an HTTP response from a GetAuditLogsWithResponse call
+func ParseGetAuditLogsResponse(rsp *http.Response) (*GetAuditLogsResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetAuditLogsResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest ListAuditResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetAuditExportResponse parses an HTTP response from a GetAuditExportWithResponse call
+func ParseGetAuditExportResponse(rsp *http.Response) (*GetAuditExportResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetAuditExportResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest ListAuditResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetAuditLogByIDResponse parses an HTTP response from a GetAuditLogByIDWithResponse call
+func ParseGetAuditLogByIDResponse(rsp *http.Response) (*GetAuditLogByIDResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetAuditLogByIDResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest AuditEntryResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON404 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetFilesResponse parses an HTTP response from a GetFilesWithResponse call
+func ParseGetFilesResponse(rsp *http.Response) (*GetFilesResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetFilesResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest FileListResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParsePostFileResponse parses an HTTP response from a PostFileWithResponse call
+func ParsePostFileResponse(rsp *http.Response) (*PostFileResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &PostFileResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201:
+ var dest FileUploadResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON201 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON409 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseDeleteFileByNameResponse parses an HTTP response from a DeleteFileByNameWithResponse call
+func ParseDeleteFileByNameResponse(rsp *http.Response) (*DeleteFileByNameResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &DeleteFileByNameResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest FileDeleteResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON404 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetFileByNameResponse parses an HTTP response from a GetFileByNameWithResponse call
+func ParseGetFileByNameResponse(rsp *http.Response) (*GetFileByNameResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetFileByNameResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest FileInfoResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON404 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetHealthResponse parses an HTTP response from a GetHealthWithResponse call
+func ParseGetHealthResponse(rsp *http.Response) (*GetHealthResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetHealthResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest HealthResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetHealthReadyResponse parses an HTTP response from a GetHealthReadyWithResponse call
+func ParseGetHealthReadyResponse(rsp *http.Response) (*GetHealthReadyResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetHealthReadyResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest ReadyResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503:
+ var dest ReadyResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON503 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetHealthStatusResponse parses an HTTP response from a GetHealthStatusWithResponse call
+func ParseGetHealthStatusResponse(rsp *http.Response) (*GetHealthStatusResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetHealthStatusResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest StatusResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503:
+ var dest StatusResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON503 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetJobResponse parses an HTTP response from a GetJobWithResponse call
+func ParseGetJobResponse(rsp *http.Response) (*GetJobResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetJobResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest ListJobsResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParsePostJobResponse parses an HTTP response from a PostJobWithResponse call
+func ParsePostJobResponse(rsp *http.Response) (*PostJobResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &PostJobResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201:
+ var dest CreateJobResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON201 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetJobStatusResponse parses an HTTP response from a GetJobStatusWithResponse call
+func ParseGetJobStatusResponse(rsp *http.Response) (*GetJobStatusResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetJobStatusResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest QueueStatsResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseDeleteJobByIDResponse parses an HTTP response from a DeleteJobByIDWithResponse call
+func ParseDeleteJobByIDResponse(rsp *http.Response) (*DeleteJobByIDResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &DeleteJobByIDResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON404 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetJobByIDResponse parses an HTTP response from a GetJobByIDWithResponse call
+func ParseGetJobByIDResponse(rsp *http.Response) (*GetJobByIDResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetJobByIDResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest JobDetailResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON404 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseRetryJobByIDResponse parses an HTTP response from a RetryJobByIDWithResponse call
+func ParseRetryJobByIDResponse(rsp *http.Response) (*RetryJobByIDResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &RetryJobByIDResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201:
+ var dest CreateJobResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON201 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON404 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetNodeStatusResponse parses an HTTP response from a GetNodeStatusWithResponse call
+func ParseGetNodeStatusResponse(rsp *http.Response) (*GetNodeStatusResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetNodeStatusResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest NodeStatusCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParsePostNodeCommandExecResponse parses an HTTP response from a PostNodeCommandExecWithResponse call
+func ParsePostNodeCommandExecResponse(rsp *http.Response) (*PostNodeCommandExecResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &PostNodeCommandExecResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202:
+ var dest CommandResultCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON202 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParsePostNodeCommandShellResponse parses an HTTP response from a PostNodeCommandShellWithResponse call
+func ParsePostNodeCommandShellResponse(rsp *http.Response) (*PostNodeCommandShellResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &PostNodeCommandShellResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202:
+ var dest CommandResultCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON202 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetNodeDiskResponse parses an HTTP response from a GetNodeDiskWithResponse call
+func ParseGetNodeDiskResponse(rsp *http.Response) (*GetNodeDiskResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetNodeDiskResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest DiskCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParsePostNodeFileDeployResponse parses an HTTP response from a PostNodeFileDeployWithResponse call
+func ParsePostNodeFileDeployResponse(rsp *http.Response) (*PostNodeFileDeployResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &PostNodeFileDeployResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202:
+ var dest FileDeployResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON202 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParsePostNodeFileStatusResponse parses an HTTP response from a PostNodeFileStatusWithResponse call
+func ParsePostNodeFileStatusResponse(rsp *http.Response) (*PostNodeFileStatusResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &PostNodeFileStatusResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest FileStatusResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetNodeHostnameResponse parses an HTTP response from a GetNodeHostnameWithResponse call
+func ParseGetNodeHostnameResponse(rsp *http.Response) (*GetNodeHostnameResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetNodeHostnameResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest HostnameCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetNodeLoadResponse parses an HTTP response from a GetNodeLoadWithResponse call
+func ParseGetNodeLoadResponse(rsp *http.Response) (*GetNodeLoadResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetNodeLoadResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest LoadCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetNodeMemoryResponse parses an HTTP response from a GetNodeMemoryWithResponse call
+func ParseGetNodeMemoryResponse(rsp *http.Response) (*GetNodeMemoryResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetNodeMemoryResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest MemoryCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParsePutNodeNetworkDNSResponse parses an HTTP response from a PutNodeNetworkDNSWithResponse call
+func ParsePutNodeNetworkDNSResponse(rsp *http.Response) (*PutNodeNetworkDNSResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &PutNodeNetworkDNSResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202:
+ var dest DNSUpdateCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON202 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetNodeNetworkDNSByInterfaceResponse parses an HTTP response from a GetNodeNetworkDNSByInterfaceWithResponse call
+func ParseGetNodeNetworkDNSByInterfaceResponse(rsp *http.Response) (*GetNodeNetworkDNSByInterfaceResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetNodeNetworkDNSByInterfaceResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest DNSConfigCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParsePostNodeNetworkPingResponse parses an HTTP response from a PostNodeNetworkPingWithResponse call
+func ParsePostNodeNetworkPingResponse(rsp *http.Response) (*PostNodeNetworkPingResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &PostNodeNetworkPingResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest PingCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetNodeOSResponse parses an HTTP response from a GetNodeOSWithResponse call
+func ParseGetNodeOSResponse(rsp *http.Response) (*GetNodeOSResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetNodeOSResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest OSInfoCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetNodeUptimeResponse parses an HTTP response from a GetNodeUptimeWithResponse call
+func ParseGetNodeUptimeResponse(rsp *http.Response) (*GetNodeUptimeResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetNodeUptimeResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest UptimeCollectionResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON403 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseGetVersionResponse parses an HTTP response from a GetVersionWithResponse call
+func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &GetVersionResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ }
+
+ return response, nil
+}
diff --git a/pkg/sdk/osapi/gen/generate.go b/pkg/sdk/osapi/gen/generate.go
new file mode 100644
index 00000000..50ce9d2c
--- /dev/null
+++ b/pkg/sdk/osapi/gen/generate.go
@@ -0,0 +1,24 @@
+// 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 gen contains generated code for the OSAPI REST API client.
+package gen
+
+//go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg.yaml ../../../../internal/api/gen/api.yaml
diff --git a/pkg/sdk/osapi/health.go b/pkg/sdk/osapi/health.go
new file mode 100644
index 00000000..723f7544
--- /dev/null
+++ b/pkg/sdk/osapi/health.go
@@ -0,0 +1,133 @@
+// 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 (
+ "context"
+ "fmt"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+// HealthService provides health check operations.
+type HealthService struct {
+ client *gen.ClientWithResponses
+}
+
+// Liveness checks if the API server process is alive.
+func (s *HealthService) Liveness(
+ ctx context.Context,
+) (*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. A 503 response is treated as success with the
+// ServiceUnavailable flag set.
+func (s *HealthService) Ready(
+ ctx context.Context,
+) (*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,
+) (*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/sdk/osapi/health_public_test.go b/pkg/sdk/osapi/health_public_test.go
new file mode 100644
index 00000000..aff3f455
--- /dev/null
+++ b/pkg/sdk/osapi/health_public_test.go
@@ -0,0 +1,373 @@
+// 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 (
+ "context"
+ "errors"
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
+)
+
+type HealthPublicTestSuite struct {
+ suite.Suite
+
+ ctx context.Context
+}
+
+func (suite *HealthPublicTestSuite) SetupTest() {
+ suite.ctx = context.Background()
+}
+
+func (suite *HealthPublicTestSuite) runner(
+ handler http.HandlerFunc,
+ serverURL string,
+) string {
+ if handler != nil {
+ server := httptest.NewServer(handler)
+ suite.T().Cleanup(server.Close)
+ return server.URL
+ }
+
+ return serverURL
+}
+
+func (suite *HealthPublicTestSuite) TestLiveness() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ validateFunc func(*osapi.Response[osapi.HealthStatus], 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() {
+ sut := osapi.New(
+ suite.runner(tc.handler, tc.serverURL),
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Health.Liveness(suite.ctx)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *HealthPublicTestSuite) TestReady() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ validateFunc func(*osapi.Response[osapi.ReadyStatus], 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")
+ },
+ },
+ {
+ 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() {
+ sut := osapi.New(
+ suite.runner(tc.handler, tc.serverURL),
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Health.Ready(suite.ctx)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *HealthPublicTestSuite) TestStatus() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ validateFunc func(*osapi.Response[osapi.SystemStatus], 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")
+ },
+ },
+ {
+ 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")
+ },
+ },
+ {
+ 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() {
+ sut := osapi.New(
+ suite.runner(tc.handler, tc.serverURL),
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Health.Status(suite.ctx)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func TestHealthPublicTestSuite(t *testing.T) {
+ suite.Run(t, new(HealthPublicTestSuite))
+}
diff --git a/pkg/sdk/osapi/health_types.go b/pkg/sdk/osapi/health_types.go
new file mode 100644
index 00000000..074e3faa
--- /dev/null
+++ b/pkg/sdk/osapi/health_types.go
@@ -0,0 +1,286 @@
+// 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/retr0h/osapi/pkg/sdk/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
+ ObjectStores []ObjectStoreInfo
+}
+
+// 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
+}
+
+// ObjectStoreInfo represents an Object Store bucket's info.
+type ObjectStoreInfo struct {
+ Name string
+ Size 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
+ }
+
+ if g.ObjectStores != nil {
+ stores := make([]ObjectStoreInfo, 0, len(*g.ObjectStores))
+ for _, o := range *g.ObjectStores {
+ stores = append(stores, ObjectStoreInfo{
+ Name: o.Name,
+ Size: o.Size,
+ })
+ }
+
+ s.ObjectStores = stores
+ }
+
+ return s
+}
diff --git a/pkg/sdk/osapi/health_types_test.go b/pkg/sdk/osapi/health_types_test.go
new file mode 100644
index 00000000..0f68e254
--- /dev/null
+++ b/pkg/sdk/osapi/health_types_test.go
@@ -0,0 +1,351 @@
+// 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/retr0h/osapi/pkg/sdk/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,
+ },
+ },
+ ObjectStores: &[]gen.ObjectStoreInfo{
+ {
+ Name: "file-objects",
+ Size: 5242880,
+ },
+ },
+ }
+ }(),
+ 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)
+
+ suite.Require().Len(s.ObjectStores, 1)
+ suite.Equal("file-objects", s.ObjectStores[0].Name)
+ suite.Equal(5242880, s.ObjectStores[0].Size)
+ },
+ },
+ {
+ 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)
+ suite.Nil(s.ObjectStores)
+ },
+ },
+ {
+ 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/sdk/osapi/job.go b/pkg/sdk/osapi/job.go
new file mode 100644
index 00000000..8965cc50
--- /dev/null
+++ b/pkg/sdk/osapi/job.go
@@ -0,0 +1,226 @@
+// 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 (
+ "context"
+ "fmt"
+
+ "github.com/google/uuid"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+// JobService provides job queue operations.
+type JobService struct {
+ client *gen.ClientWithResponses
+}
+
+// Create creates a new job with the given operation and target.
+func (s *JobService) Create(
+ ctx context.Context,
+ operation map[string]interface{},
+ target string,
+) (*Response[JobCreated], error) {
+ body := gen.CreateJobRequest{
+ Operation: operation,
+ TargetHostname: target,
+ }
+
+ 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,
+) (*Response[JobDetail], error) {
+ parsedID, err := uuid.Parse(id)
+ if err != nil {
+ return nil, fmt.Errorf("invalid job ID: %w", err)
+ }
+
+ 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,
+) error {
+ parsedID, err := uuid.Parse(id)
+ if err != nil {
+ 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)
+ }
+
+ 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.
+type ListParams struct {
+ // Status filters by job status (e.g., "pending", "completed").
+ Status string
+
+ // Limit is the maximum number of results. Zero uses server default.
+ Limit int
+
+ // Offset is the number of results to skip. Zero starts from the
+ // beginning.
+ Offset int
+}
+
+// List retrieves jobs, optionally filtered by status.
+func (s *JobService) List(
+ ctx context.Context,
+ params ListParams,
+) (*Response[JobList], error) {
+ p := &gen.GetJobParams{}
+
+ if params.Status != "" {
+ status := gen.GetJobParamsStatus(params.Status)
+ p.Status = &status
+ }
+
+ if params.Limit > 0 {
+ p.Limit = ¶ms.Limit
+ }
+
+ if params.Offset > 0 {
+ p.Offset = ¶ms.Offset
+ }
+
+ 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,
+) (*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.
+func (s *JobService) Retry(
+ ctx context.Context,
+ id string,
+ target string,
+) (*Response[JobCreated], error) {
+ parsedID, err := uuid.Parse(id)
+ if err != nil {
+ return nil, fmt.Errorf("invalid job ID: %w", err)
+ }
+
+ body := gen.RetryJobByIDJSONRequestBody{}
+ if target != "" {
+ body.TargetHostname = &target
+ }
+
+ 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/sdk/osapi/job_public_test.go b/pkg/sdk/osapi/job_public_test.go
new file mode 100644
index 00000000..af3c5978
--- /dev/null
+++ b/pkg/sdk/osapi/job_public_test.go
@@ -0,0 +1,691 @@
+// 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 (
+ "context"
+ "errors"
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
+)
+
+type JobPublicTestSuite struct {
+ suite.Suite
+
+ ctx context.Context
+}
+
+func (suite *JobPublicTestSuite) SetupTest() {
+ suite.ctx = context.Background()
+}
+
+func (suite *JobPublicTestSuite) TestCreate() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ operation map[string]interface{}
+ target string
+ validateFunc func(*osapi.Response[osapi.JobCreated], error)
+ }{
+ {
+ name: "when creating job returns response",
+ handler: 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"}`,
+ ),
+ )
+ },
+ operation: map[string]interface{}{"type": "system.hostname.get"},
+ target: "_any",
+ 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 server returns 400 returns ValidationError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"error":"validation failed"}`))
+ },
+ operation: map[string]interface{}{},
+ target: "_any",
+ 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)
+ },
+ },
+ {
+ name: "when HTTP request fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ operation: map[string]interface{}{},
+ target: "_any",
+ validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "create job")
+ },
+ },
+ {
+ name: "when server returns 201 with empty body returns UnexpectedStatusError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusCreated)
+ },
+ operation: map[string]interface{}{},
+ target: "_any",
+ 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() {
+ var (
+ serverURL string
+ server *httptest.Server
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ } else {
+ server = httptest.NewServer(tc.handler)
+ defer server.Close()
+ serverURL = server.URL
+ }
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Job.Create(suite.ctx, tc.operation, tc.target)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *JobPublicTestSuite) TestGet() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ id string
+ validateFunc func(*osapi.Response[osapi.JobDetail], error)
+ }{
+ {
+ name: "when valid UUID returns response",
+ handler: 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"}`,
+ ),
+ )
+ },
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ 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",
+ handler: 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"}`,
+ ),
+ )
+ },
+ id: "not-a-uuid",
+ validateFunc: func(resp *osapi.Response[osapi.JobDetail], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "invalid job ID")
+ },
+ },
+ {
+ name: "when HTTP request fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ id: "00000000-0000-0000-0000-000000000000",
+ validateFunc: func(resp *osapi.Response[osapi.JobDetail], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "get job")
+ },
+ },
+ {
+ name: "when server returns 200 with empty body returns UnexpectedStatusError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ id: "00000000-0000-0000-0000-000000000000",
+ 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")
+ },
+ },
+ {
+ name: "when server returns 404 returns NotFoundError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"error":"job not found"}`))
+ },
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ 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() {
+ var (
+ serverURL string
+ server *httptest.Server
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ } else {
+ server = httptest.NewServer(tc.handler)
+ defer server.Close()
+ serverURL = server.URL
+ }
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Job.Get(suite.ctx, tc.id)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *JobPublicTestSuite) TestDelete() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ id string
+ validateFunc func(error)
+ }{
+ {
+ name: "when valid UUID returns no error",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusNoContent)
+ },
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ validateFunc: func(err error) {
+ suite.NoError(err)
+ },
+ },
+ {
+ name: "when invalid UUID returns error",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusNoContent)
+ },
+ id: "not-a-uuid",
+ validateFunc: func(err error) {
+ suite.Error(err)
+ suite.Contains(err.Error(), "invalid job ID")
+ },
+ },
+ {
+ name: "when HTTP request fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ id: "00000000-0000-0000-0000-000000000000",
+ validateFunc: func(err error) {
+ suite.Error(err)
+ suite.Contains(err.Error(), "delete job")
+ },
+ },
+ {
+ name: "when server returns 404 returns NotFoundError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"error":"job not found"}`))
+ },
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ 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() {
+ var (
+ serverURL string
+ server *httptest.Server
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ } else {
+ server = httptest.NewServer(tc.handler)
+ defer server.Close()
+ serverURL = server.URL
+ }
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ err := sut.Job.Delete(suite.ctx, tc.id)
+ tc.validateFunc(err)
+ })
+ }
+}
+
+func (suite *JobPublicTestSuite) TestList() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ params osapi.ListParams
+ validateFunc func(*osapi.Response[osapi.JobList], error)
+ }{
+ {
+ name: "when no filters returns response",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"items":[],"total_items":0}`))
+ },
+ params: osapi.ListParams{},
+ 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 response",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"items":[],"total_items":0}`))
+ },
+ params: osapi.ListParams{
+ Status: "completed",
+ Limit: 10,
+ Offset: 5,
+ },
+ validateFunc: func(resp *osapi.Response[osapi.JobList], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ },
+ },
+ {
+ name: "when HTTP request fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ params: osapi.ListParams{},
+ validateFunc: func(resp *osapi.Response[osapi.JobList], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "list jobs")
+ },
+ },
+ {
+ name: "when server returns 401 returns AuthError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusUnauthorized)
+ _, _ = w.Write([]byte(`{"error":"unauthorized"}`))
+ },
+ params: osapi.ListParams{},
+ 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)
+ },
+ },
+ {
+ name: "when server returns 200 with empty body returns UnexpectedStatusError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ params: osapi.ListParams{},
+ 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() {
+ var (
+ serverURL string
+ server *httptest.Server
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ } else {
+ server = httptest.NewServer(tc.handler)
+ defer server.Close()
+ serverURL = server.URL
+ }
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Job.List(suite.ctx, tc.params)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *JobPublicTestSuite) TestQueueStats() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ validateFunc func(*osapi.Response[osapi.QueueStats], error)
+ }{
+ {
+ name: "when requesting queue stats returns response",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"total_jobs":5}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.QueueStats], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.Equal(5, resp.Data.TotalJobs)
+ },
+ },
+ {
+ name: "when HTTP request fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.QueueStats], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "queue stats")
+ },
+ },
+ {
+ name: "when server returns 401 returns AuthError",
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when server returns 200 with empty body returns UnexpectedStatusError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ 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() {
+ var (
+ serverURL string
+ server *httptest.Server
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ } else {
+ server = httptest.NewServer(tc.handler)
+ defer server.Close()
+ serverURL = server.URL
+ }
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Job.QueueStats(suite.ctx)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *JobPublicTestSuite) TestRetry() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ id string
+ target string
+ validateFunc func(*osapi.Response[osapi.JobCreated], error)
+ }{
+ {
+ name: "when valid UUID with empty target returns response",
+ handler: 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"}`,
+ ),
+ )
+ },
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ target: "",
+ 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 response",
+ handler: 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"}`,
+ ),
+ )
+ },
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ target: "web-01",
+ validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ },
+ },
+ {
+ name: "when invalid UUID returns error",
+ handler: 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"}`,
+ ),
+ )
+ },
+ id: "not-a-uuid",
+ target: "",
+ validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "invalid job ID")
+ },
+ },
+ {
+ name: "when HTTP request fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ id: "00000000-0000-0000-0000-000000000000",
+ target: "",
+ validateFunc: func(resp *osapi.Response[osapi.JobCreated], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "retry job")
+ },
+ },
+ {
+ name: "when server returns 404 returns NotFoundError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"error":"job not found"}`))
+ },
+ id: "00000000-0000-0000-0000-000000000000",
+ target: "",
+ 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)
+ },
+ },
+ {
+ name: "when server returns 201 with empty body returns UnexpectedStatusError",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusCreated)
+ },
+ id: "00000000-0000-0000-0000-000000000000",
+ target: "",
+ 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() {
+ var (
+ serverURL string
+ server *httptest.Server
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ } else {
+ server = httptest.NewServer(tc.handler)
+ defer server.Close()
+ serverURL = server.URL
+ }
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Job.Retry(suite.ctx, tc.id, tc.target)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func TestJobPublicTestSuite(t *testing.T) {
+ suite.Run(t, new(JobPublicTestSuite))
+}
diff --git a/pkg/sdk/osapi/job_types.go b/pkg/sdk/osapi/job_types.go
new file mode 100644
index 00000000..6d3ee5a8
--- /dev/null
+++ b/pkg/sdk/osapi/job_types.go
@@ -0,0 +1,262 @@
+// 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/retr0h/osapi/pkg/sdk/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
+}
+
+// JobList is a paginated list of jobs.
+type JobList struct {
+ Items []JobDetail
+ TotalItems int
+ StatusCounts map[string]int
+}
+
+// QueueStats represents job queue statistics.
+type QueueStats struct {
+ TotalJobs int
+ DlqCount int
+ StatusCounts 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.StatusCounts != nil {
+ jl.StatusCounts = *g.StatusCounts
+ }
+
+ 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
+ }
+
+ return qs
+}
diff --git a/pkg/sdk/osapi/job_types_test.go b/pkg/sdk/osapi/job_types_test.go
new file mode 100644
index 00000000..430c99ab
--- /dev/null
+++ b/pkg/sdk/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/retr0h/osapi/pkg/sdk/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
+ statusCounts := map[string]int{
+ "pending": 1,
+ "completed": 0,
+ }
+ items := []gen.JobDetailResponse{
+ {
+ Id: &id,
+ Status: &status,
+ },
+ }
+
+ return &gen.ListJobsResponse{
+ Items: &items,
+ TotalItems: &totalItems,
+ StatusCounts: &statusCounts,
+ }
+ }(),
+ validateFunc: func(jl JobList) {
+ suite.Equal(1, jl.TotalItems)
+ suite.Equal(map[string]int{
+ "pending": 1,
+ "completed": 0,
+ }, jl.StatusCounts)
+ 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.StatusCounts)
+ 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,
+ }
+
+ return &gen.QueueStatsResponse{
+ TotalJobs: &totalJobs,
+ DlqCount: &dlqCount,
+ StatusCounts: &statusCounts,
+ }
+ }(),
+ 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)
+ },
+ },
+ {
+ 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)
+ },
+ },
+ }
+
+ 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/sdk/osapi/metrics.go b/pkg/sdk/osapi/metrics.go
new file mode 100644
index 00000000..2af459a0
--- /dev/null
+++ b/pkg/sdk/osapi/metrics.go
@@ -0,0 +1,66 @@
+// 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 (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+// MetricsService provides Prometheus metrics access.
+type MetricsService struct {
+ client *gen.ClientWithResponses
+ baseURL string
+}
+
+// Get fetches the raw Prometheus metrics text from the /metrics endpoint.
+func (s *MetricsService) Get(
+ ctx context.Context,
+) (string, error) {
+ url := strings.TrimRight(s.baseURL, "/") + "/metrics"
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ if err != nil {
+ return "", fmt.Errorf("creating metrics request: %w", err)
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("fetching metrics: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("metrics endpoint returned status %d", resp.StatusCode)
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("reading metrics response: %w", err)
+ }
+
+ return string(body), nil
+}
diff --git a/pkg/sdk/osapi/metrics_public_test.go b/pkg/sdk/osapi/metrics_public_test.go
new file mode 100644
index 00000000..ec31309c
--- /dev/null
+++ b/pkg/sdk/osapi/metrics_public_test.go
@@ -0,0 +1,137 @@
+// 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 (
+ "context"
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
+)
+
+type MetricsPublicTestSuite struct {
+ suite.Suite
+
+ ctx context.Context
+}
+
+func (suite *MetricsPublicTestSuite) SetupTest() {
+ suite.ctx = context.Background()
+}
+
+func (suite *MetricsPublicTestSuite) TestGet() {
+ closedServer := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }),
+ )
+ closedServerURL := closedServer.URL
+ closedServer.Close()
+
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ ctx context.Context
+ validateFunc func(string, error)
+ }{
+ {
+ name: "when server returns metrics returns text body",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte("# HELP go_goroutines\n"))
+ },
+ ctx: suite.ctx,
+ validateFunc: func(body string, err error) {
+ suite.NoError(err)
+ suite.Equal("# HELP go_goroutines\n", body)
+ },
+ },
+ {
+ name: "when server returns non-200 returns error",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ },
+ ctx: suite.ctx,
+ validateFunc: func(body string, err error) {
+ suite.Error(err)
+ suite.Contains(err.Error(), "metrics endpoint returned status")
+ suite.Empty(body)
+ },
+ },
+ {
+ name: "when server is unreachable returns error",
+ serverURL: closedServerURL,
+ ctx: suite.ctx,
+ validateFunc: func(body string, err error) {
+ suite.Error(err)
+ suite.Contains(err.Error(), "fetching metrics")
+ suite.Empty(body)
+ },
+ },
+ {
+ name: "when request creation fails returns error",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ ctx: nil,
+ validateFunc: func(body string, err error) {
+ suite.Error(err)
+ suite.Contains(err.Error(), "creating metrics request")
+ suite.Empty(body)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ var targetURL string
+
+ if tc.serverURL != "" {
+ targetURL = tc.serverURL
+ } else {
+ server := httptest.NewServer(tc.handler)
+ defer server.Close()
+ targetURL = server.URL
+ }
+
+ sut := osapi.New(
+ targetURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ //nolint:staticcheck // nil context intentionally triggers NewRequestWithContext error
+ body, err := sut.Metrics.Get(tc.ctx)
+ tc.validateFunc(body, err)
+ })
+ }
+}
+
+func TestMetricsPublicTestSuite(t *testing.T) {
+ suite.Run(t, new(MetricsPublicTestSuite))
+}
diff --git a/pkg/sdk/osapi/metrics_test.go b/pkg/sdk/osapi/metrics_test.go
new file mode 100644
index 00000000..9560777a
--- /dev/null
+++ b/pkg/sdk/osapi/metrics_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
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+)
+
+type errorReader struct{}
+
+func (e *errorReader) Read(
+ _ []byte,
+) (int, error) {
+ return 0, fmt.Errorf("read error")
+}
+
+func (e *errorReader) Close() error {
+ return nil
+}
+
+type MetricsTestSuite struct {
+ suite.Suite
+}
+
+func (s *MetricsTestSuite) TestGetReadBodyError() {
+ tests := []struct {
+ name string
+ validateFunc func(string, error)
+ }{
+ {
+ name: "when body read fails returns error",
+ validateFunc: func(body string, err error) {
+ s.Error(err)
+ s.Contains(err.Error(), "reading metrics response")
+ s.Empty(body)
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ s.Run(tt.name, func() {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Length", "100")
+ w.WriteHeader(http.StatusOK)
+ }),
+ )
+ defer server.Close()
+
+ sut := &MetricsService{
+ baseURL: server.URL,
+ }
+
+ origTransport := http.DefaultTransport
+ http.DefaultTransport = &readErrorTransport{}
+ defer func() { http.DefaultTransport = origTransport }()
+
+ body, err := sut.Get(context.Background())
+ tt.validateFunc(body, err)
+ })
+ }
+}
+
+type readErrorTransport struct{}
+
+func (t *readErrorTransport) RoundTrip(
+ req *http.Request,
+) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(&errorReader{}),
+ Request: req,
+ }, nil
+}
+
+func TestMetricsTestSuite(t *testing.T) {
+ suite.Run(t, new(MetricsTestSuite))
+}
diff --git a/pkg/sdk/osapi/node.go b/pkg/sdk/osapi/node.go
new file mode 100644
index 00000000..655370ca
--- /dev/null
+++ b/pkg/sdk/osapi/node.go
@@ -0,0 +1,512 @@
+// 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 (
+ "context"
+ "fmt"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+// NodeService provides node management operations.
+type NodeService struct {
+ client *gen.ClientWithResponses
+}
+
+// ExecRequest contains parameters for direct command execution.
+type ExecRequest struct {
+ // Command is the binary to execute (required).
+ Command string
+
+ // Args is the argument list passed to the command.
+ Args []string
+
+ // Cwd is the working directory. Empty uses the agent default.
+ Cwd string
+
+ // Timeout in seconds. Zero uses the server default (30s).
+ Timeout int
+
+ // Target specifies the host: "_any", "_all", hostname, or
+ // label ("group:web").
+ Target string
+}
+
+// FileDeployOpts contains parameters for file deployment.
+type FileDeployOpts struct {
+ // ObjectName is the name of the file in the Object Store (required).
+ ObjectName string
+
+ // Path is the destination path on the target filesystem (required).
+ Path string
+
+ // ContentType is "raw" or "template" (required).
+ ContentType string
+
+ // Mode is the file permission mode (e.g., "0644"). Optional.
+ Mode string
+
+ // Owner is the file owner user. Optional.
+ Owner string
+
+ // Group is the file owner group. Optional.
+ Group string
+
+ // Vars are template variables when ContentType is "template". Optional.
+ Vars map[string]any
+
+ // Target specifies the host: "_any", "_all", hostname, or
+ // label ("group:web").
+ Target string
+}
+
+// ShellRequest contains parameters for shell command execution.
+type ShellRequest struct {
+ // Command is the shell command string passed to /bin/sh -c (required).
+ Command string
+
+ // Cwd is the working directory. Empty uses the agent default.
+ Cwd string
+
+ // Timeout in seconds. Zero uses the server default (30s).
+ Timeout int
+
+ // Target specifies the host: "_any", "_all", hostname, or
+ // label ("group:web").
+ Target string
+}
+
+// Status retrieves node status (OS info, disk, memory, load) from the
+// target host.
+func (s *NodeService) Status(
+ ctx context.Context,
+ target string,
+) (*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,
+) (*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,
+) (*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,
+) (*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,
+) (*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,
+) (*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,
+) (*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
+// target host.
+func (s *NodeService) GetDNS(
+ ctx context.Context,
+ target string,
+ interfaceName string,
+) (*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
+// target host.
+func (s *NodeService) UpdateDNS(
+ ctx context.Context,
+ target string,
+ interfaceName string,
+ servers []string,
+ searchDomains []string,
+) (*Response[Collection[DNSUpdateResult]], error) {
+ body := gen.DNSConfigUpdateRequest{
+ InterfaceName: interfaceName,
+ }
+
+ if len(servers) > 0 {
+ body.Servers = &servers
+ }
+
+ if len(searchDomains) > 0 {
+ body.SearchDomains = &searchDomains
+ }
+
+ 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.
+func (s *NodeService) Ping(
+ ctx context.Context,
+ target string,
+ address string,
+) (*Response[Collection[PingResult]], error) {
+ body := gen.PostNodeNetworkPingJSONRequestBody{
+ Address: address,
+ }
+
+ 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,
+) (*Response[Collection[CommandResult]], error) {
+ body := gen.CommandExecRequest{
+ Command: req.Command,
+ }
+
+ if len(req.Args) > 0 {
+ body.Args = &req.Args
+ }
+
+ if req.Cwd != "" {
+ body.Cwd = &req.Cwd
+ }
+
+ if req.Timeout > 0 {
+ body.Timeout = &req.Timeout
+ }
+
+ 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
+// (pipes, redirects, variable expansion).
+func (s *NodeService) Shell(
+ ctx context.Context,
+ req ShellRequest,
+) (*Response[Collection[CommandResult]], error) {
+ body := gen.CommandShellRequest{
+ Command: req.Command,
+ }
+
+ if req.Cwd != "" {
+ body.Cwd = &req.Cwd
+ }
+
+ if req.Timeout > 0 {
+ body.Timeout = &req.Timeout
+ }
+
+ 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
+}
+
+// FileDeploy deploys a file from the Object Store to the target host.
+func (s *NodeService) FileDeploy(
+ ctx context.Context,
+ req FileDeployOpts,
+) (*Response[FileDeployResult], error) {
+ body := gen.FileDeployRequest{
+ ObjectName: req.ObjectName,
+ Path: req.Path,
+ ContentType: gen.FileDeployRequestContentType(req.ContentType),
+ }
+
+ if req.Mode != "" {
+ body.Mode = &req.Mode
+ }
+
+ if req.Owner != "" {
+ body.Owner = &req.Owner
+ }
+
+ if req.Group != "" {
+ body.Group = &req.Group
+ }
+
+ if len(req.Vars) > 0 {
+ body.Vars = &req.Vars
+ }
+
+ resp, err := s.client.PostNodeFileDeployWithResponse(ctx, req.Target, body)
+ if err != nil {
+ return nil, fmt.Errorf("file deploy: %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(fileDeployResultFromGen(resp.JSON202), resp.Body), nil
+}
+
+// FileStatus checks the deployment status of a file on the target host.
+func (s *NodeService) FileStatus(
+ ctx context.Context,
+ target string,
+ path string,
+) (*Response[FileStatusResult], error) {
+ body := gen.FileStatusRequest{
+ Path: path,
+ }
+
+ resp, err := s.client.PostNodeFileStatusWithResponse(ctx, target, body)
+ if err != nil {
+ return nil, fmt.Errorf("file 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(fileStatusResultFromGen(resp.JSON200), resp.Body), nil
+}
diff --git a/pkg/sdk/osapi/node_public_test.go b/pkg/sdk/osapi/node_public_test.go
new file mode 100644
index 00000000..4df60658
--- /dev/null
+++ b/pkg/sdk/osapi/node_public_test.go
@@ -0,0 +1,1693 @@
+// 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 (
+ "context"
+ "errors"
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
+)
+
+type NodePublicTestSuite struct {
+ suite.Suite
+
+ ctx context.Context
+}
+
+func (suite *NodePublicTestSuite) SetupTest() {
+ suite.ctx = context.Background()
+}
+
+func (suite *NodePublicTestSuite) TestHostname() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ target string
+ validateFunc func(*osapi.Response[osapi.Collection[osapi.HostnameResult]], error)
+ }{
+ {
+ name: "when requesting hostname returns results",
+ target: "_any",
+ handler: 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"}]}`,
+ ),
+ )
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.HostnameResult]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "get hostname")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.Hostname(suite.ctx, tc.target)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestStatus() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ target string
+ validateFunc func(*osapi.Response[osapi.Collection[osapi.NodeStatus]], error)
+ }{
+ {
+ name: "when requesting status returns results",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"results":[{"hostname":"web-01"}]}`))
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.NodeStatus]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "get status")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.Status(suite.ctx, tc.target)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestDisk() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ target string
+ validateFunc func(*osapi.Response[osapi.Collection[osapi.DiskResult]], error)
+ }{
+ {
+ name: "when requesting disk returns results",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"results":[{"hostname":"disk-host"}]}`))
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DiskResult]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "get disk")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.Disk(suite.ctx, tc.target)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestMemory() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ target string
+ validateFunc func(*osapi.Response[osapi.Collection[osapi.MemoryResult]], error)
+ }{
+ {
+ name: "when requesting memory returns results",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"results":[{"hostname":"mem-host"}]}`))
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.MemoryResult]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "get memory")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ 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)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.Memory(suite.ctx, tc.target)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestLoad() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ target string
+ validateFunc func(*osapi.Response[osapi.Collection[osapi.LoadResult]], error)
+ }{
+ {
+ name: "when requesting load returns results",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"results":[{"hostname":"load-host"}]}`))
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.LoadResult]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "get load")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.Load(suite.ctx, tc.target)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestOS() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ target string
+ validateFunc func(*osapi.Response[osapi.Collection[osapi.OSInfoResult]], error)
+ }{
+ {
+ name: "when requesting OS info returns results",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"results":[{"hostname":"os-host"}]}`))
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.OSInfoResult]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "get os")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.OS(suite.ctx, tc.target)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestUptime() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ target string
+ validateFunc func(*osapi.Response[osapi.Collection[osapi.UptimeResult]], error)
+ }{
+ {
+ name: "when requesting uptime returns results",
+ target: "_any",
+ handler: 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"}]}`),
+ )
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.UptimeResult]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "get uptime")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.Uptime(suite.ctx, tc.target)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestGetDNS() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ target string
+ interfaceName string
+ validateFunc func(*osapi.Response[osapi.Collection[osapi.DNSConfig]], error)
+ }{
+ {
+ name: "when requesting DNS returns results",
+ target: "_any",
+ interfaceName: "eth0",
+ handler: 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"]}]}`),
+ )
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ interfaceName: "eth0",
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ interfaceName: "eth0",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSConfig]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "get dns")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ interfaceName: "eth0",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.GetDNS(suite.ctx, tc.target, tc.interfaceName)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestUpdateDNS() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL 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,
+ handler: 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}]}`,
+ ),
+ )
+ },
+ 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"},
+ handler: 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}]}`,
+ ),
+ )
+ },
+ 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"},
+ handler: 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}]}`,
+ ),
+ )
+ },
+ 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,
+ handler: 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}]}`,
+ ),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ iface: "eth0",
+ servers: []string{"8.8.8.8"},
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ iface: "eth0",
+ servers: []string{"8.8.8.8"},
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.DNSUpdateResult]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "update dns")
+ },
+ },
+ {
+ name: "when server returns 202 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ iface: "eth0",
+ servers: []string{"8.8.8.8"},
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusAccepted)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "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) TestPing() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL 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",
+ handler: 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}]}`,
+ ),
+ )
+ },
+ 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)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ address: "8.8.8.8",
+ handler: 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.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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ address: "8.8.8.8",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.PingResult]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "ping")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ address: "8.8.8.8",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.Ping(suite.ctx, tc.target, tc.address)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestExec() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL 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",
+ },
+ handler: 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}]}`,
+ ),
+ )
+ },
+ 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",
+ },
+ handler: 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}]}`,
+ ),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ },
+ },
+ {
+ name: "when server returns 400 returns ValidationError",
+ req: osapi.ExecRequest{
+ Target: "_any",
+ },
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"error":"command is required"}`))
+ },
+ 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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ req: osapi.ExecRequest{
+ Command: "whoami",
+ Target: "_any",
+ },
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "exec command")
+ },
+ },
+ {
+ name: "when server returns 202 with no JSON body returns UnexpectedStatusError",
+ req: osapi.ExecRequest{
+ Command: "whoami",
+ Target: "_any",
+ },
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusAccepted)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.Exec(suite.ctx, tc.req)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestShell() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL 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",
+ },
+ handler: 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}]}`,
+ ),
+ )
+ },
+ 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",
+ },
+ handler: 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}]}`,
+ ),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ },
+ },
+ {
+ name: "when server returns 400 returns ValidationError",
+ req: osapi.ShellRequest{
+ Target: "_any",
+ },
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"error":"command is required"}`))
+ },
+ 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)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ req: osapi.ShellRequest{
+ Command: "uname -a",
+ Target: "_any",
+ },
+ validateFunc: func(resp *osapi.Response[osapi.Collection[osapi.CommandResult]], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "shell command")
+ },
+ },
+ {
+ name: "when server returns 202 with no JSON body returns UnexpectedStatusError",
+ req: osapi.ShellRequest{
+ Command: "uname -a",
+ Target: "_any",
+ },
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusAccepted)
+ },
+ 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.Shell(suite.ctx, tc.req)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestFileDeploy() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ req osapi.FileDeployOpts
+ validateFunc func(*osapi.Response[osapi.FileDeployResult], error)
+ }{
+ {
+ name: "when deploying file returns result",
+ req: osapi.FileDeployOpts{
+ ObjectName: "nginx.conf",
+ Path: "/etc/nginx/nginx.conf",
+ ContentType: "raw",
+ Target: "_any",
+ },
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ _, _ = w.Write(
+ []byte(
+ `{"job_id":"job-123","hostname":"web-01","changed":true}`,
+ ),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileDeployResult], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.Equal("job-123", resp.Data.JobID)
+ suite.Equal("web-01", resp.Data.Hostname)
+ suite.True(resp.Data.Changed)
+ },
+ },
+ {
+ name: "when all options provided returns results",
+ req: osapi.FileDeployOpts{
+ ObjectName: "app.conf.tmpl",
+ Path: "/etc/app/app.conf",
+ ContentType: "template",
+ Mode: "0644",
+ Owner: "root",
+ Group: "root",
+ Vars: map[string]any{"port": 8080},
+ Target: "web-01",
+ },
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ _, _ = w.Write(
+ []byte(
+ `{"job_id":"job-456","hostname":"web-01","changed":true}`,
+ ),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileDeployResult], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ },
+ },
+ {
+ name: "when server returns 400 returns ValidationError",
+ req: osapi.FileDeployOpts{
+ Target: "_any",
+ },
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"error":"object_name is required"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileDeployResult], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.ValidationError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusBadRequest, target.StatusCode)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ req: osapi.FileDeployOpts{
+ ObjectName: "nginx.conf",
+ Path: "/etc/nginx/nginx.conf",
+ ContentType: "raw",
+ Target: "_any",
+ },
+ handler: 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.FileDeployResult], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.AuthError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusForbidden, target.StatusCode)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ serverURL: "http://127.0.0.1:0",
+ req: osapi.FileDeployOpts{
+ ObjectName: "nginx.conf",
+ Path: "/etc/nginx/nginx.conf",
+ ContentType: "raw",
+ Target: "_any",
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileDeployResult], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "file deploy")
+ },
+ },
+ {
+ name: "when server returns 202 with no JSON body returns UnexpectedStatusError",
+ req: osapi.FileDeployOpts{
+ ObjectName: "nginx.conf",
+ Path: "/etc/nginx/nginx.conf",
+ ContentType: "raw",
+ Target: "_any",
+ },
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusAccepted)
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileDeployResult], 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.FileDeploy(suite.ctx, tc.req)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func (suite *NodePublicTestSuite) TestFileStatus() {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ serverURL string
+ target string
+ path string
+ validateFunc func(*osapi.Response[osapi.FileStatusResult], error)
+ }{
+ {
+ name: "when checking file status returns result",
+ target: "_any",
+ path: "/etc/nginx/nginx.conf",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(
+ []byte(
+ `{"job_id":"job-789","hostname":"web-01","path":"/etc/nginx/nginx.conf","status":"in-sync","sha256":"abc123"}`,
+ ),
+ )
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileStatusResult], err error) {
+ suite.NoError(err)
+ suite.NotNil(resp)
+ suite.Equal("job-789", resp.Data.JobID)
+ suite.Equal("web-01", resp.Data.Hostname)
+ suite.Equal("/etc/nginx/nginx.conf", resp.Data.Path)
+ suite.Equal("in-sync", resp.Data.Status)
+ suite.Equal("abc123", resp.Data.SHA256)
+ },
+ },
+ {
+ name: "when server returns 400 returns ValidationError",
+ target: "_any",
+ path: "",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"error":"path is required"}`))
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileStatusResult], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.ValidationError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusBadRequest, target.StatusCode)
+ },
+ },
+ {
+ name: "when server returns 403 returns AuthError",
+ target: "_any",
+ path: "/etc/nginx/nginx.conf",
+ handler: 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.FileStatusResult], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+
+ var target *osapi.AuthError
+ suite.True(errors.As(err, &target))
+ suite.Equal(http.StatusForbidden, target.StatusCode)
+ },
+ },
+ {
+ name: "when client HTTP call fails returns error",
+ target: "_any",
+ path: "/etc/nginx/nginx.conf",
+ serverURL: "http://127.0.0.1:0",
+ validateFunc: func(resp *osapi.Response[osapi.FileStatusResult], err error) {
+ suite.Error(err)
+ suite.Nil(resp)
+ suite.Contains(err.Error(), "file status")
+ },
+ },
+ {
+ name: "when server returns 200 with no JSON body returns UnexpectedStatusError",
+ target: "_any",
+ path: "/etc/nginx/nginx.conf",
+ handler: func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ },
+ validateFunc: func(resp *osapi.Response[osapi.FileStatusResult], 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() {
+ var (
+ serverURL string
+ cleanup func()
+ )
+
+ if tc.serverURL != "" {
+ serverURL = tc.serverURL
+ cleanup = func() {}
+ } else {
+ server := httptest.NewServer(tc.handler)
+ serverURL = server.URL
+ cleanup = server.Close
+ }
+ defer cleanup()
+
+ sut := osapi.New(
+ serverURL,
+ "test-token",
+ osapi.WithLogger(slog.Default()),
+ )
+
+ resp, err := sut.Node.FileStatus(suite.ctx, tc.target, tc.path)
+ tc.validateFunc(resp, err)
+ })
+ }
+}
+
+func TestNodePublicTestSuite(t *testing.T) {
+ suite.Run(t, new(NodePublicTestSuite))
+}
diff --git a/pkg/sdk/osapi/node_types.go b/pkg/sdk/osapi/node_types.go
new file mode 100644
index 00000000..b54f6390
--- /dev/null
+++ b/pkg/sdk/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/retr0h/osapi/pkg/sdk/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/sdk/osapi/node_types_test.go b/pkg/sdk/osapi/node_types_test.go
new file mode 100644
index 00000000..b884bf49
--- /dev/null
+++ b/pkg/sdk/osapi/node_types_test.go
@@ -0,0 +1,974 @@
+// 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/retr0h/osapi/pkg/sdk/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) TestDerefString() {
+ s := "hello"
+
+ tests := []struct {
+ name string
+ input *string
+ validateFunc func(string)
+ }{
+ {
+ name: "when pointer is non-nil",
+ input: &s,
+ validateFunc: func(result string) {
+ suite.Equal("hello", result)
+ },
+ },
+ {
+ name: "when pointer is nil",
+ input: nil,
+ validateFunc: func(result string) {
+ suite.Empty(result)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ tc.validateFunc(derefString(tc.input))
+ })
+ }
+}
+
+func (suite *NodeTypesTestSuite) TestDerefInt() {
+ i := 42
+
+ tests := []struct {
+ name string
+ input *int
+ validateFunc func(int)
+ }{
+ {
+ name: "when pointer is non-nil",
+ input: &i,
+ validateFunc: func(result int) {
+ suite.Equal(42, result)
+ },
+ },
+ {
+ name: "when pointer is nil",
+ input: nil,
+ validateFunc: func(result int) {
+ suite.Zero(result)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ tc.validateFunc(derefInt(tc.input))
+ })
+ }
+}
+
+func (suite *NodeTypesTestSuite) TestDerefInt64() {
+ i := int64(42)
+
+ tests := []struct {
+ name string
+ input *int64
+ validateFunc func(int64)
+ }{
+ {
+ name: "when pointer is non-nil",
+ input: &i,
+ validateFunc: func(result int64) {
+ suite.Equal(int64(42), result)
+ },
+ },
+ {
+ name: "when pointer is nil",
+ input: nil,
+ validateFunc: func(result int64) {
+ suite.Zero(result)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ tc.validateFunc(derefInt64(tc.input))
+ })
+ }
+}
+
+func (suite *NodeTypesTestSuite) TestDerefFloat64() {
+ f := 3.14
+
+ tests := []struct {
+ name string
+ input *float64
+ validateFunc func(float64)
+ }{
+ {
+ name: "when pointer is non-nil",
+ input: &f,
+ validateFunc: func(result float64) {
+ suite.InDelta(3.14, result, 0.001)
+ },
+ },
+ {
+ name: "when pointer is nil",
+ input: nil,
+ validateFunc: func(result float64) {
+ suite.InDelta(0.0, result, 0.001)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ tc.validateFunc(derefFloat64(tc.input))
+ })
+ }
+}
+
+func (suite *NodeTypesTestSuite) TestDerefBool() {
+ b := true
+
+ tests := []struct {
+ name string
+ input *bool
+ validateFunc func(bool)
+ }{
+ {
+ name: "when pointer is non-nil",
+ input: &b,
+ validateFunc: func(result bool) {
+ suite.True(result)
+ },
+ },
+ {
+ name: "when pointer is nil",
+ input: nil,
+ validateFunc: func(result bool) {
+ suite.False(result)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ tc.validateFunc(derefBool(tc.input))
+ })
+ }
+}
+
+func (suite *NodeTypesTestSuite) TestJobIDFromGen() {
+ id := openapi_types.UUID{
+ 0x55, 0x0e, 0x84, 0x00,
+ 0xe2, 0x9b, 0x41, 0xd4,
+ 0xa7, 0x16, 0x44, 0x66,
+ 0x55, 0x44, 0x00, 0x00,
+ }
+
+ tests := []struct {
+ name string
+ input *openapi_types.UUID
+ validateFunc func(string)
+ }{
+ {
+ name: "when pointer is non-nil",
+ input: &id,
+ validateFunc: func(result string) {
+ suite.Equal("550e8400-e29b-41d4-a716-446655440000", result)
+ },
+ },
+ {
+ name: "when pointer is nil",
+ input: nil,
+ validateFunc: func(result string) {
+ suite.Empty(result)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ tc.validateFunc(jobIDFromGen(tc.input))
+ })
+ }
+}
+
+func TestNodeTypesTestSuite(t *testing.T) {
+ suite.Run(t, new(NodeTypesTestSuite))
+}
diff --git a/pkg/sdk/osapi/osapi.go b/pkg/sdk/osapi/osapi.go
new file mode 100644
index 00000000..54136f86
--- /dev/null
+++ b/pkg/sdk/osapi/osapi.go
@@ -0,0 +1,140 @@
+// 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 provides a Go SDK for the OSAPI REST API.
+//
+// Create a client with New() and use the domain-specific services
+// to interact with the API:
+//
+// client := osapi.New("http://localhost:8080", "your-jwt-token")
+//
+// // Get hostname
+// resp, err := client.Node.Hostname(ctx, "_any")
+//
+// // Execute a command
+// resp, err := client.Node.Exec(ctx, osapi.ExecRequest{
+// Command: "uptime",
+// Target: "_all",
+// })
+package osapi
+
+import (
+ "log/slog"
+ "net/http"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+// Client is the top-level OSAPI SDK client. Use New() to create one.
+type Client struct {
+ // Agent provides agent discovery and details operations.
+ Agent *AgentService
+
+ // Node provides node management operations (hostname, status, disk,
+ // memory, load, OS, uptime, network DNS/ping, command exec/shell).
+ Node *NodeService
+
+ // Job provides job queue operations (create, get, list, delete, retry).
+ Job *JobService
+
+ // Health provides health check operations (liveness, readiness, status).
+ Health *HealthService
+
+ // Audit provides audit log operations (list, get, export).
+ Audit *AuditService
+
+ // Metrics provides Prometheus metrics access.
+ Metrics *MetricsService
+
+ // File provides file management operations (upload, list, get, delete).
+ File *FileService
+
+ httpClient *gen.ClientWithResponses
+ baseURL string
+ logger *slog.Logger
+ baseTransport http.RoundTripper
+}
+
+// Option configures the Client.
+type Option func(*Client)
+
+// WithLogger sets a custom logger. Defaults to slog.Default().
+func WithLogger(
+ logger *slog.Logger,
+) Option {
+ return func(c *Client) {
+ c.logger = logger
+ }
+}
+
+// WithHTTPTransport sets a custom base HTTP transport.
+func WithHTTPTransport(
+ transport http.RoundTripper,
+) Option {
+ return func(c *Client) {
+ c.baseTransport = transport
+ }
+}
+
+// New creates an OSAPI SDK client.
+func New(
+ baseURL string,
+ bearerToken string,
+ opts ...Option,
+) *Client {
+ c := &Client{
+ baseURL: baseURL,
+ logger: slog.Default(),
+ baseTransport: http.DefaultTransport,
+ }
+
+ for _, opt := range opts {
+ opt(c)
+ }
+
+ transport := &authTransport{
+ base: c.baseTransport,
+ authHeader: "Bearer " + bearerToken,
+ logger: c.logger,
+ }
+
+ hc := &http.Client{
+ Transport: transport,
+ }
+
+ // Error is unreachable: the only ClientOption passed (WithHTTPClient) cannot
+ // fail, and NewClientWithResponses only errors when a ClientOption does.
+ // Invalid URLs are caught later at HTTP call time with a clear parse error.
+ httpClient, _ := gen.NewClientWithResponses(baseURL, gen.WithHTTPClient(hc))
+
+ c.httpClient = httpClient
+ c.Agent = &AgentService{client: httpClient}
+ c.Node = &NodeService{client: httpClient}
+ c.Job = &JobService{client: httpClient}
+ c.Health = &HealthService{client: httpClient}
+ c.Audit = &AuditService{client: httpClient}
+ c.Metrics = &MetricsService{
+ client: httpClient,
+ baseURL: baseURL,
+ }
+ c.File = &FileService{client: httpClient}
+
+ return c
+}
diff --git a/pkg/sdk/osapi/osapi_public_test.go b/pkg/sdk/osapi/osapi_public_test.go
new file mode 100644
index 00000000..f572c613
--- /dev/null
+++ b/pkg/sdk/osapi/osapi_public_test.go
@@ -0,0 +1,101 @@
+// 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 (
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ "github.com/retr0h/osapi/pkg/sdk/osapi"
+)
+
+type ClientPublicTestSuite struct {
+ suite.Suite
+
+ server *httptest.Server
+}
+
+func (suite *ClientPublicTestSuite) SetupTest() {
+ 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(`{}`))
+ }),
+ )
+}
+
+func (suite *ClientPublicTestSuite) TearDownTest() {
+ suite.server.Close()
+}
+
+func (suite *ClientPublicTestSuite) TestNew() {
+ tests := []struct {
+ name string
+ opts func() []osapi.Option
+ validateFunc func(*osapi.Client)
+ }{
+ {
+ name: "when creating client returns all services",
+ opts: func() []osapi.Option {
+ return nil
+ },
+ validateFunc: func(c *osapi.Client) {
+ suite.NotNil(c)
+ suite.NotNil(c.Node)
+ suite.NotNil(c.Job)
+ suite.NotNil(c.Health)
+ suite.NotNil(c.Audit)
+ suite.NotNil(c.Metrics)
+ suite.NotNil(c.File)
+ },
+ },
+ {
+ name: "when custom transport provided creates client",
+ opts: func() []osapi.Option {
+ return []osapi.Option{
+ osapi.WithHTTPTransport(&http.Transport{}),
+ osapi.WithLogger(slog.Default()),
+ }
+ },
+ validateFunc: func(c *osapi.Client) {
+ suite.NotNil(c)
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ c := osapi.New(suite.server.URL, "test-token", tc.opts()...)
+ tc.validateFunc(c)
+ })
+ }
+}
+
+func TestClientPublicTestSuite(
+ t *testing.T,
+) {
+ suite.Run(t, new(ClientPublicTestSuite))
+}
diff --git a/pkg/sdk/osapi/response.go b/pkg/sdk/osapi/response.go
new file mode 100644
index 00000000..b02f1423
--- /dev/null
+++ b/pkg/sdk/osapi/response.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 osapi
+
+import (
+ "fmt"
+
+ "github.com/retr0h/osapi/pkg/sdk/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 409:
+ return &ConflictError{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/sdk/osapi/response_public_test.go b/pkg/sdk/osapi/response_public_test.go
new file mode 100644
index 00000000..b5ac9a64
--- /dev/null
+++ b/pkg/sdk/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/retr0h/osapi/pkg/sdk/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/sdk/osapi/response_test.go b/pkg/sdk/osapi/response_test.go
new file mode 100644
index 00000000..182288be
--- /dev/null
+++ b/pkg/sdk/osapi/response_test.go
@@ -0,0 +1,208 @@
+// 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/retr0h/osapi/pkg/sdk/osapi/gen"
+)
+
+type ResponseTestSuite struct {
+ suite.Suite
+}
+
+func (suite *ResponseTestSuite) TestCheckError() {
+ tests := []struct {
+ name string
+ statusCode int
+ validateFunc func(error)
+ }{
+ {
+ name: "when status is 200",
+ statusCode: 200,
+ validateFunc: func(err error) {
+ suite.NoError(err)
+ },
+ },
+ {
+ name: "when status is 201",
+ statusCode: 201,
+ validateFunc: func(err error) {
+ suite.NoError(err)
+ },
+ },
+ {
+ name: "when status is 202",
+ statusCode: 202,
+ validateFunc: func(err error) {
+ suite.NoError(err)
+ },
+ },
+ {
+ name: "when status is 204",
+ statusCode: 204,
+ validateFunc: func(err error) {
+ suite.NoError(err)
+ },
+ },
+ {
+ name: "when status is 400",
+ statusCode: 400,
+ validateFunc: func(err error) {
+ suite.Error(err)
+ var target *ValidationError
+ suite.True(errors.As(err, &target))
+ suite.Equal(400, target.StatusCode)
+ },
+ },
+ {
+ name: "when status is 401",
+ statusCode: 401,
+ validateFunc: func(err error) {
+ suite.Error(err)
+ 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) {
+ suite.Error(err)
+ var target *AuthError
+ suite.True(errors.As(err, &target))
+ suite.Equal(403, target.StatusCode)
+ },
+ },
+ {
+ name: "when status is 404",
+ statusCode: 404,
+ validateFunc: func(err error) {
+ suite.Error(err)
+ var target *NotFoundError
+ suite.True(errors.As(err, &target))
+ suite.Equal(404, target.StatusCode)
+ },
+ },
+ {
+ name: "when status is 409",
+ statusCode: 409,
+ validateFunc: func(err error) {
+ suite.Error(err)
+ var target *ConflictError
+ suite.True(errors.As(err, &target))
+ suite.Equal(409, target.StatusCode)
+ },
+ },
+ {
+ name: "when status is 500",
+ statusCode: 500,
+ validateFunc: func(err error) {
+ suite.Error(err)
+ var target *ServerError
+ suite.True(errors.As(err, &target))
+ suite.Equal(500, target.StatusCode)
+ },
+ },
+ {
+ name: "when status is 503",
+ statusCode: 503,
+ validateFunc: func(err error) {
+ suite.Error(err)
+ 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)
+ tc.validateFunc(err)
+ })
+ }
+}
+
+func (suite *ResponseTestSuite) TestCheckErrorMessages() {
+ 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.Error(err)
+ suite.Contains(err.Error(), "field 'name' is required")
+ },
+ },
+ {
+ name: "when all responses are nil",
+ statusCode: 400,
+ responses: []*gen.ErrorResponse{nil, nil},
+ validateFunc: func(err error) {
+ suite.Error(err)
+ suite.Contains(err.Error(), "unexpected status 400")
+ },
+ },
+ {
+ name: "when no responses are provided",
+ statusCode: 500,
+ responses: nil,
+ validateFunc: func(err error) {
+ suite.Error(err)
+ 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.Error(err)
+ suite.Contains(err.Error(), "unexpected status 404")
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ suite.Run(tc.name, func() {
+ err := checkError(tc.statusCode, tc.responses...)
+ tc.validateFunc(err)
+ })
+ }
+}
+
+func TestResponseTestSuite(t *testing.T) {
+ suite.Run(t, new(ResponseTestSuite))
+}
diff --git a/pkg/sdk/osapi/transport.go b/pkg/sdk/osapi/transport.go
new file mode 100644
index 00000000..323a7f15
--- /dev/null
+++ b/pkg/sdk/osapi/transport.go
@@ -0,0 +1,67 @@
+// 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 (
+ "log/slog"
+ "net/http"
+ "time"
+
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/propagation"
+)
+
+type authTransport struct {
+ base http.RoundTripper
+ authHeader string
+ logger *slog.Logger
+}
+
+// RoundTrip implements the http.RoundTripper interface.
+func (t *authTransport) RoundTrip(
+ req *http.Request,
+) (*http.Response, error) {
+ req.Header.Set("Authorization", t.authHeader)
+ otel.GetTextMapPropagator().Inject(req.Context(), propagation.HeaderCarrier(req.Header))
+
+ start := time.Now()
+ resp, err := t.base.RoundTrip(req)
+ duration := time.Since(start)
+
+ if err != nil {
+ t.logger.Debug("http request failed",
+ slog.String("method", req.Method),
+ slog.String("url", req.URL.String()),
+ slog.String("error", err.Error()),
+ slog.Duration("duration", duration),
+ )
+ return nil, err
+ }
+
+ t.logger.Debug("http response",
+ slog.String("method", req.Method),
+ slog.String("url", req.URL.String()),
+ slog.Int("status", resp.StatusCode),
+ slog.Duration("duration", duration),
+ )
+
+ return resp, nil
+}
diff --git a/pkg/sdk/osapi/transport_test.go b/pkg/sdk/osapi/transport_test.go
new file mode 100644
index 00000000..c5e2cfb9
--- /dev/null
+++ b/pkg/sdk/osapi/transport_test.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 osapi
+
+import (
+ "fmt"
+ "log/slog"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+)
+
+type failingRoundTripper struct{}
+
+func (f *failingRoundTripper) RoundTrip(
+ _ *http.Request,
+) (*http.Response, error) {
+ return nil, fmt.Errorf("transport error")
+}
+
+type TransportTestSuite struct {
+ suite.Suite
+}
+
+func (s *TransportTestSuite) TestRoundTripError() {
+ tests := []struct {
+ name string
+ validateFunc func(*http.Response, error)
+ }{
+ {
+ name: "when base transport fails returns error",
+ validateFunc: func(resp *http.Response, err error) {
+ s.Error(err)
+ s.Contains(err.Error(), "transport error")
+ s.Nil(resp)
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ s.Run(tt.name, func() {
+ transport := &authTransport{
+ base: &failingRoundTripper{},
+ authHeader: "Bearer test-token",
+ logger: slog.Default(),
+ }
+
+ req, err := http.NewRequest(http.MethodGet, "http://example.com/test", nil)
+ s.Require().NoError(err)
+
+ resp, err := transport.RoundTrip(req)
+ tt.validateFunc(resp, err)
+
+ s.Equal("Bearer test-token", req.Header.Get("Authorization"))
+ })
+ }
+}
+
+func TestTransportTestSuite(t *testing.T) {
+ suite.Run(t, new(TransportTestSuite))
+}
diff --git a/pkg/sdk/osapi/types.go b/pkg/sdk/osapi/types.go
new file mode 100644
index 00000000..eae7db17
--- /dev/null
+++ b/pkg/sdk/osapi/types.go
@@ -0,0 +1,31 @@
+// 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
+
+// TimelineEvent represents a lifecycle event. Used by both job
+// timelines and agent state transition history.
+type TimelineEvent struct {
+ Timestamp string
+ Event string
+ Hostname string
+ Message string
+ Error string
+}