diff --git a/api/v1alpha1/agent/openapi.yaml b/api/v1alpha1/agent/openapi.yaml index f8254b3..63c33e3 100644 --- a/api/v1alpha1/agent/openapi.yaml +++ b/api/v1alpha1/agent/openapi.yaml @@ -110,12 +110,13 @@ components: type: string inventory: $ref: '../openapi.yaml#/components/schemas/Inventory' - credentialUrl: + agentId: type: string + format: uuid required: - status - statusInfo - - credentialUrl + - agentId AgentStatusUpdate: type: object properties: diff --git a/api/v1alpha1/agent/spec.gen.go b/api/v1alpha1/agent/spec.gen.go index 82a57c1..cb03c1b 100644 --- a/api/v1alpha1/agent/spec.gen.go +++ b/api/v1alpha1/agent/spec.gen.go @@ -19,27 +19,28 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/9RYT1PjNhT/Kh61RycOdE+5QcruZrZQBobdA5ODsF5iLbakSs9kKOPv3pFkYydWnJBl", - "2+kNpKf35/d+74/zQlJZKClAoCHTF2LSDArq/jxbgcBbpFiaO8Uogj1UWirQyMGJpBoYCOQ0v9O5PcBn", - "BWRKDGouVqSKCWfBY+PUDlzNxVIGr59AGy5F4K6KiYa/Sq6Bkel9Y2JDYbzlsfOv1bmIG53y4TukaO1d", - "aC11P/ICjKErBwkDk2qu0Hnl5aPmOt7jZCO3qGIyF0tN+5YYRWpQav8fRyhMX2ipAWZU0ZTj86fzDjhc", - "IKxA20hQIs33CrmTfdi6277GeNuPEKD1AdWaPtv/M2nwWq5BW6r5aChj3MJJ8+uNKHe529FutZlr0LO8", - "NAh6A7Kdz199EYBrqR+HkBa0gCAxG+RAlEXNP8Gotvxi3Io9lAisA8kwts7OIfj5JPhwzUDmP1tkQvfb", - "9lvhbeV9ePvp64AYd7kbCmUunkCg1M99mHlTDL9qWJIp+SVp+1RSN6nEV4xtCimIOttD8l9nXsy+8Lkd", - "lL40PXAaQ15BXLsZiu2SrzS1JJ4bUw7WLjUGjClAYJBXqSw3bjp5zekD5Pur1YvFXUON2kMIditLnYab", - "P0VgZ865pdQFRTK1KYcR8iLQ++KD58WrtrJ0Dbov1mXOMEUawSreXbvGZF/gec+kagpbSBylUghIbT3H", - "ZE05crEaLaUetQFadoCbHTFZUczAKhxxwe3lqPU/JqUaoRy5AbuI3zwPSzea35CFLXI4fB0w8e6RWVvo", - "WgsxxzPlh1eGo3J77EZxxNYQCr1pLf1GxvYb5Sys8/IGjIP0XAN9ZHIt+vozblCuNC3Cm8MbB2DBxVea", - "lxCWNgjqgAnyqqR+4edAuNfY+TIwtD5K7RspfcjhULlvHLNvVAsuVmb4zZXEYfVbkbVgN64H/dzr1C4P", - "wiwIzIxUlbNmJRyeYX0KVW4heZw1c+XI936DPOJx0UzGbo6G9GyPUtvJu7DdADVSHKNG/ui6qd5tcdW0", - "OBrRfVV0UAkdXj+hhZH0TcUtS5vwXpnTpaBLwyaUOxIc4k6/ZCo3Q3yzz3kKwkC7uJMzRdMMotPxxI4z", - "O4JIhqjMNEnW6/WYuuux1KukfmuSP+azi6vbi9HpeDLOsMgdZBwtmu2aF13nVAjQ0dn1PBpF1H48RyCY", - "ktzF+PrdSkrBYMkFMMdABYIqTqbkt/FkfGJxoJg52BOqePJ0kjhVJnnhrEraKadK7H98+vEcealILiPM", - "wLtCnKma/4xMiZ/PnW98Z1rTAvx3xP227vnvG9q4PbO+NuvD1K8SLTNQlxDXPygcsNRVC/8YDJ5L5oZ/", - "KgXWazFVKuepcz/5bvz3f6t6qGb6P2NUlaewUdLm1yo4nUz6aP75xWbodHKy6+qDv3oXN/3vDM6zTVN3", - "gpaYSc3/9oz5cDL5+UY/SQGu0JHaHnBPfNoX9qjhpW9EhxFTg8ppus1MoyDlSw4s8rp6JL3xz7p75QE0", - "bbQ3Ov83VA3sz9Vmt7WO7uDuO3oQYkTD+H+BfOeURTce3v+uyiYffr7RK4kfZSnYRqXVtPWllgHNMbMG", - "VhAoK38dpRmkj73i+ezfHtrpOi7UVhfOZQP6qSk1Py8TUi2qfwIAAP//mCERXjkWAAA=", + "H4sIAAAAAAAC/9RYQVPjOBP9Ky5939GJAzun3CDLzKRmYSkoZg5UDsLu2BpsySu1SbGU//uWJCt2YsUx", + "LOzW3hKr1d16et2v7RcSi6IUHDgqMn8hKs6goObnWQocb5Fipe7KhCLoh6UUJUhkYExiCQlwZDS/k7l+", + "gM8lkDlRKBlPSR0SlngfK+N2YGnJ18K7/ARSMcE9a3VIJPxRMQkJmd+7EDsOw72MTX6tz1XofIqHnxCj", + "jnchpZD9kxegFE0NJAmoWLISTVbWPnDL4ZEknd2qDsmSryXtR0ooUoVC2n8MoVB9o7UEWNCSxgyfv5x3", + "wGEcIQWpT4ICaX7UyDw5hq1Z7XsM9/PwAdo8oFLSZ/0/EwqvxQakppo9DU0SpuGk+fXOKQ+l2/Guvalr", + "kIu8UghyB7KD27e5cMCNkI9DSHNagJeYDjngVdHwjydUan4lTJs9VAhJB5JhbE2cMfjZS7DHVQM3/1Uj", + "41vfj98a7zvvw9u/vg6IYZe7vqMs+RNwFPK5DzNzxfB/CWsyJ/+L2j4VNU0qshWjm0IMvLntIfvvC2um", + "d9i7HbS+VD1wXCDrIGzS9J3tkqWSahIvlaoGa5cqBUoVwNHLq1hUOyude83pA+THq9Wahd1Azu0Ygt2K", + "Ssae5k9TJxrbgw3Bad0YUVkiFL5IsQSKkJyZ466FLCiSuSYRTJAVnm7q9GVrW1WmoffNukwbppQzrMPD", + "ta5U9g2ejyibawRc4CQWnEOs6z8kG8qQ8XSyFnLSypFmExitCUlKMQPtcMI404uTNv+QVOUExcQI8ip8", + "tX5WRspfgfEemQy+BpjwsMQ2EbrRfEzbp4SvNETMtIfOWR6EyIHy0bfvO0DH8eHEhmcfQ//lR9LvrUPS", + "qEHIpe87vuuT/a6cHA/HDvi8vAFlYD2XQB8TseF9/xlTKFJJC/8Y9Eo1Lxj/TvMK/NYKoRwhh1snzQ4r", + "av7GqcVyQIE/C2lVgT7kMNbuB8PsB5Wc8VQN77kSOOx+72Qt2C51b55HkzqUgZ8FHgGMy2rh5tthQe5T", + "qDbT1ePCieQb99tx+A2bCyfz3Tsa8rM/F2iZ6cJ2A1QJ/hY34u/OzuW7TeGSFm9G9FgVjSqh8fXjm35J", + "P1TYstQdb8ucLgXNNexCeeCCfdzpl0xt1MO2+ZzFwBW0byHkrKRxBsHpdKa1Vr+CkwyxVPMo2mw2U2qW", + "p0KmUbNXRb8tFxdXtxeT0+lsmmGRG8gYajTbmTW4zinnIIOz62UwCYxWBMCTUjBzxu1LOKl4AmvGITEM", + "LIHTkpE5+WU6m55oHChmBvaIlix6Oons0Bi9sKSOWn0rK+y/SdvZIbBWgVgHmIFNhZhQDf8TMidWozsf", + "LExoSQuwL0X3+76Xv+54Y/qZztXNNnM7JrTMQFlB2HwdGTNzrOxmUHguEiP7seDYzPi0LHMWm/Sjn8p+", + "zGhdD9VM/5tMXVsKq1Lo+9UOTmezPpq/f9M3dDo7ObT0yS69S5r2o4nJbDfUHacVZkKyPy1jPp3MPj7o", + "F8HBFDpS3QPu7exDVvqR46VtROOIKaHMabzPTFVCzNYMksD66pH0xm7rzpYjaOq8O5//Gap6Zuh6t9vq", + "RA9w9x0z8DHCMf4fIN85TYIbC++/V2WzTx8f9ErgZ1HxZKfSGtraUsuA5pjpACl4ysouB3EG8WOveL7a", + "vWM7XSeFJurKpKxAPrlSs3oZkXpV/xUAAP//d25QjgYXAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v1alpha1/agent/types.gen.go b/api/v1alpha1/agent/types.gen.go index c9008b6..77ba1c6 100644 --- a/api/v1alpha1/agent/types.gen.go +++ b/api/v1alpha1/agent/types.gen.go @@ -5,6 +5,7 @@ package v1alpha1 import ( externalRef0 "github.com/kubev2v/migration-planner/api/v1alpha1" + openapi_types "github.com/oapi-codegen/runtime/types" ) // AgentStatusUpdate defines model for AgentStatusUpdate. @@ -18,10 +19,10 @@ type AgentStatusUpdate struct { // SourceStatusUpdate defines model for SourceStatusUpdate. type SourceStatusUpdate struct { - CredentialUrl string `json:"credentialUrl"` - Inventory *externalRef0.Inventory `json:"inventory,omitempty"` - Status string `json:"status"` - StatusInfo string `json:"statusInfo"` + AgentId openapi_types.UUID `json:"agentId"` + Inventory *externalRef0.Inventory `json:"inventory,omitempty"` + Status string `json:"status"` + StatusInfo string `json:"statusInfo"` } // UpdateAgentStatusJSONRequestBody defines body for UpdateAgentStatus for application/json ContentType. diff --git a/api/v1alpha1/openapi.yaml b/api/v1alpha1/openapi.yaml index 68df85c..953bde9 100644 --- a/api/v1alpha1/openapi.yaml +++ b/api/v1alpha1/openapi.yaml @@ -270,8 +270,6 @@ components: type: string inventory: $ref: '#/components/schemas/Inventory' - credentialUrl: - type: string createdAt: type: string format: date-time @@ -280,6 +278,10 @@ components: format: date-time sshKey: type: string + agents: + type: array + items: + $ref: '#/components/schemas/SourceAgentItem' required: - id - name @@ -302,6 +304,18 @@ components: type: array items: $ref: '#/components/schemas/Source' + + SourceAgentItem: + type: object + properties: + id: + type: string + format: uuid + associated: + type: boolean + required: + - id + - associated Error: properties: diff --git a/api/v1alpha1/spec.gen.go b/api/v1alpha1/spec.gen.go index e1d8fd0..5f4efab 100644 --- a/api/v1alpha1/spec.gen.go +++ b/api/v1alpha1/spec.gen.go @@ -18,36 +18,37 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xZX2/bOBL/KgT3gHuRrbS3Bxz85mbbrbHbNmh2sw9NHibiWOJGInXkyIav8Hc/kJRs", - "2ZJsJ01yOWzfYnE4M/zNb/6Q+coTXZRaoSLLJ1+5TTIswP85TVGR+6M0ukRDEv1nsFYnEgiF+0WrEvmE", - "32qdIyi+jnhi0C1O/da5NgUQn3ABhCOSBfKo2WPJSJXWWwQqkpD/bvKW1q2EwBzvqVSKXk1WVybB2cAi", - "AVX+lKiqgk++cKVplGilMHEHjvgSJEmVjubajLZuWx5xNEYbHvEUKEOncCSVdIsjqRaoSJsVj3hVjkiP", - "nOO88WWUaoX8JhpyZ6bmutfbqhT3RXqBxkqtetStI27w35U0Lq5fHHobOHYc2Y9WO+Btl6I2UbaGt8fU", - "t39iQs4pT7RfpfXnkISFj8DfDM75hP8Qbwka1+yMAzXXG11gDKzc77c+CB3OFmgtpOj+FGgTI0vyKAR5", - "1ixHR0Bp5G7WEZ+puYGuJQEElrQJvzaH2RWaG8RzKCGRtPr5TSsYUhGmaPzJNEF+VMh/ORZLv9rVGO37", - "0RebfYAzbelCL9FcElBdEISQDk7IL3ZOOeRuS7vTZi/QnOeVJTQ7kA1u3/iikJba3B1CWkGBvbnTINfk", - "uSVQAoyjqpBO7LZyxL2JTsPW2zkFvxCEcFx7IPLvHTJ96/v2t8L7yrvwdsPXAjFqc7fvKLNNGevALJtk", - "OJS0IWNcEUpQ1dE+JH91HsTcjuMl4eqD7YDTGAoKotrNvrN9kKkBR+KZtdXB3AVr0dqibo3dVqarnZVW", - "XHO4xfx4tgaxqG2oUXsKwS59T+n6/SRtOfTYjbaq8m2jK9ZmzmGKNILraDh3rc1+wdX/voE/R8vua8se", - "mAPduacb9zEnMOXcy9+jcg6iv+fqYEkMdu/V72tS99F9E/Ddxh6+M2kZMINUGcUWkFfI5tqwBPLcMsqA", - "mNDq79RIaBd4Fjy1Yx6dOkVMWVYVoEYGQcBtjqy1zPScUYYsBCn8kpY5vb7gjPsSxiDYMKXtGyogyaTC", - "QVPLbLVnwGEglffhmr8DmVcGr3ntz5jNaocCOtIyLEpyOtD4n0ozqQJTnTJYgMyd4TGbss/eTZbkYORc", - "omWg2PvffrtoDptogey2ciij00RML9AYKZBJ6j24PRzOGssteOyTQqbnE3bNL6skQWuvOdOmfdIx+6Dd", - "UdRcT1hGVNpJHKeSxnf/smOpHd2KSklaxYlWofFrY2OBC8xjK9MRmCSThAlVBmMopSsnrh5Irey4ED/Y", - "EpMRKDHaJGQ3MTpJ0DS3bisVJw3nfYl19eEzhivFG4NwJ/RSdfVn0pJODRT9s+s9R7BCqitHnH5pS1ie", - "MMNslNQ7wiTS3+3chHNgbHqnTWjljqKnyv0hKfsDjJIqtYf3fNR0WP3eybZgN673+nnUqSEP+lnQM7Uk", - "ZXXeXEoOT1FdCq39SHx33kw2D9wf7jAP2Fw0s1k7Rof07A9zbpZowxbK1kPU6G+98JSPdnUyUDwY0WNZ", - "dFIKnZ4/fVcW3jUVbVnaHG/DnDYFfRh2oRwIcB93uimz9hNqGNhymaCyuB2A+LSEJEP2enzmBio3B/Om", - "iSyXyzH45bE2aVzvtfGvs/O3Hy/fjl6Pz8YZFbmHTJJDc3vRYBc5KIWGTS9mrfeRCa+UwLlUKDzhSlRQ", - "Sj7h/xifjV+5YwNlHmXXiuLFqxjS5u0uRep2zlxaYrWM11dzWvAJdzPYtFkyaEvt/Hc6Xp+d+bKhFdU3", - "HSjLXCZ+b/xnPZwEmp30WOPHPQ/1rnuffnHH/PHs1aOZC09APaZ+V1BRpo38j8PWxQQcX79wjw6/cZ8a", - "UEPS1INIjtQz9IXvDFii8xyTZgBrdu5j/ZMXv9ysPhnc9UT8QrEO8PgXtGG6DmHoSPQcCG4vKC8fxVLb", - "HhjDLZBBDWUHyXDru2wWXXlGS2+0WD0yivX1cr3bBMhUuO5E8NUj2+6DNPgjQgjPnj6Eb0CwzwHdF0Sb", - "bqWLv0qxPqncDTCqXd98mzJQYHjk/LKva/bT5lbcyEv33fW25n1jEt46djkTtaA58uq0vnnyCnGoOvwl", - "qOWM/vj0Rj9qeqcrdb9GYhBEoFiJiZxLFEPM/YwgvvP2O2+fmbcDNTiWRf3G2EvrFMkT8NPVlM1lHt4z", - "dxi5S+6fsZ6YZkX4D+f/G8F1QkgjSwbDk1WPnVupwL/N71vqRGSqPHAB4u98fzK+R/yfz4HsTBEaBTm7", - "RLNAwxrBTrZFPITcdYsMQXTzKmvaxamJ9X7TNV58Zp2UA6dw9iSOHeXEN8fQVc4MIadssEyGZZZkmNz1", - "hS73qB8Hy7XFliu11RvPResdDsEOD0MxX9+s/xsAAP//UDwuHFIlAAA=", + "H4sIAAAAAAAC/+xZX2/bOBL/KgT3gHuRrbS3Bxz85mbbrbHbNmh2sw9tHibi2OJGInXkKIGv8Hc/kJRs", + "2aJsJW16PWzfYpGcP7/5zR8yn3imy0orVGT57BO3WY4l+D/nK1Tk/qiMrtCQRP8ZrNWZBELhftG6Qj7j", + "N1oXCIpvEp4ZdItzf3SpTQnEZ1wA4YRkiTxpz1gyUq2aIwIVSSh+N0VH6m6HwAIfKFSKqCSra5PhYmCR", + "gGrvJaq65LMPXGmaZFopzJzDCb8HSVKtJkttJjuzLU84GqMNT/gKKEcncCKVdIsTqe5QkTZrnvC6mpCe", + "OMN5a8tkpRXy62TInIVa6qi1dSUeivQdGiu1iojbJNzgv2tpXFw/OPS2cOwZchitbsC7JiVdouwU79zU", + "N39iRs4oT7RfpfV+SMLSR+BvBpd8xn9IdwRNG3amgZqbrSwwBtbu90sfhB5nS7QWVuj+FGgzIyvyKIT9", + "rF1OToDS7rveJHyhlgb6mgQQWNIm/No6s79paRDPoYJM0vrnF51gSEW4QuM90wTFyU3+y6lY+tW+xOTQ", + "jlhsDgHOtaULfY/mkoCagiCEdHBCcbHn5ZC5HelOmr1Ac17UltDsQTZ4fGuLQrrX5vYY0gpKjOZOi1yb", + "55ZACTCOqkK6bTe1I+51Mg5br2cMfiEIwV17JPKvHTKx9UP9u82Hwvvw9sPXATHpcjfmymJbxnowyzYZ", + "jiVtyBhXhDJUTbSP7b86D9vcidMl4eqN7YHTKgoCksbMmG9v5MqAI/HC2vpo7oK1aG3ZtMZ+K9P13kon", + "rgXcYHE6W8O2pKuoFTuGYJe+p0TsXrUtflSFDWJ8nV0QljFNj2j0oSdv99a1bzP9bV2mHadUu3GTDOe6", + "tfkvuP7fN/yv0eJjbdwDc6SbR7p3jGmHlHjwZDgq+jEHOoKHDTv3jjygBQzS4sCEwdoe9D5ocGmyM5a3", + "WybuTyjhO5OWATNItVHsDooa2VIblkFRWEY5EBNa/Z3aHdoxkgVL7ZQnY8ehOcvrEtTEIAi4KZB1lple", + "MsqRBfaEX9IyJ9dXzmkskw2CDePmoaISslwqHFR1n68PFDgMpPI2fOSvQBa1wY+8sWfKFo1BAR1pGZYV", + "ORlo/E+lmVSBfE4Y3IEsnOIpm7P33kyWFWDkUqJloNjr3367aJ3NtEB2UzuU0Ukipu/QGCmQSYo6bo+H", + "s8FyBx57p5Dp5Yx95Jd1lqG1HznTpuvplL3RzhW11DOWE1V2lqYrSdPbf9mp1I5uZa0krdNMqzDBaGNT", + "gXdYpFauJmCyXBJmVBtMoZKuzrlCJbWy01L8YCvMJqDEZFsp+onRS4K2S/dnAjHqlhFLrKs37zHcjV4Y", + "hFuh71Vffi4t6ZWBMj6EP3CWLKW6csSJ77aE1YhhbCukORFGqnjbdqPakfnvlTZhJnEUHbvvD0n5H2CU", + "VCt7/MxbTcfFH3i2A7s1PWrnSaOGLIizIDJ+ZVV93t6ujo+DfQpt/Gx/e96OaI88Hy5jjzhctkNmN0bH", + "5BxOpW7I6cIWytZjxOjPvblVX+wOaKB8NKKnsmhUCo3Pn9jdi/dVJTuWtu5tmdOloA/DPpQDAY5xp58y", + "Gz86h0mykBkqi7sBiM8ryHJkz6dnbtIzBZ/xtonc399PwS9PtVmlzVmb/ro4f/n28uXk+fRsmlNZeMgk", + "OTR3NyZ2UYBSaNj8YtF56JnxWglcSoXCE65CBZXkM/6P6dn0mXMbKPcou1aU3j1LdzeUFVK/cxbSEmv2", + "eHkNpwWfcTeDzdslg7bSzn4n4/nZmS8bWlFzZYOqKmTmz6Z/NsNJoNmoVyc/7nmo981794tz88ezZ19M", + "XXjLiqj6XUFNuTbyPw5bFxNwfP0Q7nj82n1qQQ1J0wwiBVJk6AvfGbBMFwVm7QDWnjzE+ie//XK7+mRw", + "NxPxN4p1gMc/BQ7TdQhDR6KvgeDugvLto1hpG4ExXE8ZNFD2kAy3vst20ZVntPRCi/UXRrG5Xm72mwCZ", + "Gje9CD77wrpjkAZ7RAjh2dOH8AUI9j6g+w3Rpl/p0k9SbEaVuwFGdeubb1MGSgyvtR8OZS1+2t6K2/3S", + "fXe9rX14mYU3jH3OJB1oTj2IXD95hThWHf4S1HJKf3x6pW81vdK1elgjMQgiUKzCTC4liiHmvkcQ33n7", + "nbdfmbcDNTiVZfPGGKX1CskT8N3VnC1lEd4z9xi5T+6fsZmYFmX4V+3/G8F1RkgTSwbDk1VEz41U4P9p", + "cKipF5G58sAFiL/z/cn4nvB/fg1kF4rQKCjYJZo7NKzd2Mu2hIeQu26RI4h+XuVtuxibWK+3XeObz6xR", + "OTCGs6M4dpITnx1DVzlzhILywTIZllmWY3YbC13hUT8NlmuLHVMardeei9YbHIIdHoZSvrne/DcAAP//", + "fTeSZxsmAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v1alpha1/types.gen.go b/api/v1alpha1/types.gen.go index 591d59b..126a426 100644 --- a/api/v1alpha1/types.gen.go +++ b/api/v1alpha1/types.gen.go @@ -96,20 +96,26 @@ type MigrationIssues = []struct { // Source defines model for Source. type Source struct { - CreatedAt time.Time `json:"createdAt"` - CredentialUrl *string `json:"credentialUrl,omitempty"` - Id openapi_types.UUID `json:"id"` - Inventory *Inventory `json:"inventory,omitempty"` - Name string `json:"name"` - SshKey *string `json:"sshKey,omitempty"` - Status SourceStatus `json:"status"` - StatusInfo string `json:"statusInfo"` - UpdatedAt time.Time `json:"updatedAt"` + Agents *[]SourceAgentItem `json:"agents,omitempty"` + CreatedAt time.Time `json:"createdAt"` + Id openapi_types.UUID `json:"id"` + Inventory *Inventory `json:"inventory,omitempty"` + Name string `json:"name"` + SshKey *string `json:"sshKey,omitempty"` + Status SourceStatus `json:"status"` + StatusInfo string `json:"statusInfo"` + UpdatedAt time.Time `json:"updatedAt"` } // SourceStatus defines model for Source.Status. type SourceStatus string +// SourceAgentItem defines model for SourceAgentItem. +type SourceAgentItem struct { + Associated bool `json:"associated"` + Id openapi_types.UUID `json:"id"` +} + // SourceCreate defines model for SourceCreate. type SourceCreate struct { Name string `json:"name"` diff --git a/cmd/planner-agent/main.go b/cmd/planner-agent/main.go index 582f905..9420514 100644 --- a/cmd/planner-agent/main.go +++ b/cmd/planner-agent/main.go @@ -6,10 +6,15 @@ import ( "fmt" "os" + "github.com/google/uuid" "github.com/kubev2v/migration-planner/internal/agent" "github.com/kubev2v/migration-planner/pkg/log" ) +var ( + agentID string +) + func main() { command := NewAgentCommand() if err := command.Execute(); err != nil { @@ -30,6 +35,7 @@ func NewAgentCommand() *agentCmd { } flag.StringVar(&a.configFile, "config", agent.DefaultConfigFile, "Path to the agent's configuration file.") + flag.StringVar(&agentID, "id", os.Getenv("AGENT_ID"), "ID of the agent") flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0]) @@ -52,7 +58,7 @@ func NewAgentCommand() *agentCmd { } func (a *agentCmd) Execute() error { - agentInstance := agent.New(a.log, a.config) + agentInstance := agent.New(uuid.MustParse(agentID), a.log, a.config) if err := agentInstance.Run(context.Background()); err != nil { a.log.Fatalf("running device agent: %v", err) } diff --git a/internal/agent/agent.go b/internal/agent/agent.go index d1bd3f9..d44b69e 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -2,6 +2,7 @@ package agent import ( "context" + "errors" "fmt" "net" "net/url" @@ -10,6 +11,8 @@ import ( "syscall" "time" + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1" "github.com/kubev2v/migration-planner/internal/agent/client" "github.com/kubev2v/migration-planner/pkg/log" "github.com/lthibault/jitterbug" @@ -28,11 +31,12 @@ const ( var version string // New creates a new agent. -func New(log *log.PrefixLogger, config *Config) *Agent { +func New(id uuid.UUID, log *log.PrefixLogger, config *Config) *Agent { return &Agent{ config: config, log: log, healtCheckStopCh: make(chan chan any), + id: id, } } @@ -42,6 +46,7 @@ type Agent struct { server *Server healtCheckStopCh chan chan any credUrl string + id uuid.UUID } func (a *Agent) GetLogPrefix() string { @@ -67,7 +72,10 @@ func (a *Agent) Run(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) a.start(ctx, client) - <-sig + select { + case <-sig: + case <-ctx.Done(): + } a.log.Info("stopping agent...") @@ -91,9 +99,12 @@ func (a *Agent) Stop() { } func (a *Agent) start(ctx context.Context, plannerClient client.Planner) { + inventoryUpdater := NewInventoryUpdater(a.log, a.id, plannerClient) + statusUpdater := NewStatusUpdater(a.log, a.id, version, a.credUrl, a.config, plannerClient) + // start server a.server = NewServer(defaultAgentPort, a.config.DataDir, a.config.WwwDir) - go a.server.Start(a.log) + go a.server.Start(a.log, statusUpdater) // get the credentials url a.initializeCredentialUrl() @@ -115,9 +126,15 @@ func (a *Agent) start(ctx context.Context, plannerClient client.Planner) { collector := NewCollector(a.log, a.config.DataDir) collector.collect(ctx) - inventoryUpdater := NewInventoryUpdater(a.log, a.config, a.credUrl, plannerClient) updateTicker := jitterbug.New(time.Duration(a.config.UpdateInterval.Duration), &jitterbug.Norm{Stdev: 30 * time.Millisecond, Mean: 0}) + /* + Main loop + The status of agent is always computed even if we don't have connectivity with the backend. + If we're connected to the backend, the agent sends its status and if the status is UpToDate, + it sends the inventory. + In case of "source gone", it stops everything and break from the loop. + */ go func() { for { select { @@ -126,16 +143,30 @@ func (a *Agent) start(ctx context.Context, plannerClient client.Planner) { case <-updateTicker.C: } + // calculate status regardless if we have connectivity withe the backend. + status, statusInfo, inventory := statusUpdater.CalculateStatus() + // check for health. Send requests only if we have connectivity if healthChecker.State() == HealthCheckStateConsoleUnreachable { continue } - // set the status - inventoryUpdater.UpdateServiceWithInventory(ctx) + if err := statusUpdater.UpdateStatus(ctx, status, statusInfo); err != nil { + if errors.Is(err, client.ErrSourceGone) { + a.log.Info("Source is gone..Stop sending requests") + // stop the server and the healthchecker + a.Stop() + break + } + a.log.Errorf("unable to update agent status: %s", err) + continue // skip inventory update if we cannot update agent's state. + } + + if status == api.AgentStatusUpToDate { + inventoryUpdater.UpdateServiceWithInventory(ctx, api.SourceStatusUpToDate, "Inventory collected with success", inventory) + } } }() - } func (a *Agent) initializeCredentialUrl() { diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go new file mode 100644 index 0000000..31a08b8 --- /dev/null +++ b/internal/agent/agent_test.go @@ -0,0 +1,107 @@ +package agent_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "time" + + "github.com/google/uuid" + agentapi "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "github.com/kubev2v/migration-planner/internal/agent" + "github.com/kubev2v/migration-planner/internal/agent/client" + "github.com/kubev2v/migration-planner/internal/util" + "github.com/kubev2v/migration-planner/pkg/log" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Agent", func() { + var ( + // test http server used to get the cred url + testHttpServer *httptest.Server + agentTmpFolder string + agentID uuid.UUID + logger *log.PrefixLogger + endpointsCalled map[string]any + ) + + BeforeEach(func() { + logger = log.NewPrefixLogger("") + agentID, _ = uuid.NewUUID() + endpointsCalled = make(map[string]any) + + testHttpServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.String(), "/api/v1/agents") { + // save the response + body, err := io.ReadAll(r.Body) + Expect(err).To(BeNil()) + + status := agentapi.AgentStatusUpdate{} + + err = json.Unmarshal(body, &status) + Expect(err).To(BeNil()) + endpointsCalled[r.URL.String()] = status + w.WriteHeader(http.StatusOK) + return + } + + endpointsCalled[r.URL.String()] = true + w.WriteHeader(http.StatusOK) + })) + var err error + agentTmpFolder, err = os.MkdirTemp("", "agent-data-folder") + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + testHttpServer.Close() + os.RemoveAll(agentTmpFolder) + }) + + Context("Agent", func() { + It("agents starts successfully -- status waiting-for-credentials", func() { + updateInterval, _ := time.ParseDuration("5s") + config := agent.Config{ + PlannerService: agent.PlannerService{Config: *client.NewDefault()}, + DataDir: agentTmpFolder, + ConfigDir: agentTmpFolder, + UpdateInterval: util.Duration{Duration: updateInterval}, + HealthCheckInterval: 10, + } + config.PlannerService.Service.Server = testHttpServer.URL + + a := agent.New(agentID, logger, &config) + ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) + go func() { + err := a.Run(ctx) + Expect(err).To(BeNil()) + }() + + <-time.After(30 * time.Second) + cancel() + + select { + case <-ctx.Done(): + Expect(ctx.Err().Error()).To(Equal("context canceled")) + case <-time.After(20 * time.Second): + Fail("agent did not returned when context was canceled") + } + + // We should have calles to /health and /agents endpoint + status, found := endpointsCalled[fmt.Sprintf("/api/v1/agents/%s/status", agentID)] + Expect(found).To(BeTrue()) + Expect(status.(agentapi.AgentStatusUpdate).CredentialUrl).NotTo(BeEmpty()) + Expect(status.(agentapi.AgentStatusUpdate).Status).To(Equal("waiting-for-credentials")) + + _, found = endpointsCalled["/health"] + Expect(found).To(BeTrue()) + }) + }) + +}) diff --git a/internal/agent/client/client.go b/internal/agent/client/client.go index fc228a2..5a9a29c 100644 --- a/internal/agent/client/client.go +++ b/internal/agent/client/client.go @@ -34,9 +34,12 @@ func NewDefault() *Config { return baseclient.NewDefault() } +//go:generate moq -out zz_generated_planner.go . Planner // Planner is the client interface for migration planning. type Planner interface { - UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate, rcb ...client.RequestEditorFn) error + UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error // Health is checking the connectivity with console.redhat.com by making requests to /health endpoint. Health(ctx context.Context) error + // UpdateAgentStatus updates the agent status. + UpdateAgentStatus(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error } diff --git a/internal/agent/client/planner.go b/internal/agent/client/planner.go index e640801..ae68f70 100644 --- a/internal/agent/client/planner.go +++ b/internal/agent/client/planner.go @@ -15,6 +15,7 @@ var _ Planner = (*planner)(nil) var ( ErrEmptyResponse = errors.New("empty response") + ErrSourceGone = errors.New("source is gone") ) func NewPlanner(client *client.ClientWithResponses) Planner { @@ -27,8 +28,8 @@ type planner struct { client *client.ClientWithResponses } -func (p *planner) UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate, rcb ...client.RequestEditorFn) error { - resp, err := p.client.ReplaceSourceStatusWithResponse(ctx, id, params, rcb...) +func (p *planner) UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error { + resp, err := p.client.ReplaceSourceStatusWithResponse(ctx, id, params) if err != nil { return err } @@ -55,3 +56,20 @@ func (p *planner) Health(ctx context.Context) error { } return nil } + +func (p *planner) UpdateAgentStatus(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error { + resp, err := p.client.UpdateAgentStatusWithResponse(ctx, id, params) + if err != nil { + return err + } + if resp.HTTPResponse != nil { + defer resp.HTTPResponse.Body.Close() + } + if resp.StatusCode() == http.StatusGone { + return ErrSourceGone + } + if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusCreated { + return fmt.Errorf("update agent status failed with status: %s", resp.Status()) + } + return nil +} diff --git a/internal/agent/client/zz_generated_planner.go b/internal/agent/client/zz_generated_planner.go new file mode 100644 index 0000000..19fff97 --- /dev/null +++ b/internal/agent/client/zz_generated_planner.go @@ -0,0 +1,189 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package client + +import ( + "context" + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "sync" +) + +// Ensure, that PlannerMock does implement Planner. +// If this is not the case, regenerate this file with moq. +var _ Planner = &PlannerMock{} + +// PlannerMock is a mock implementation of Planner. +// +// func TestSomethingThatUsesPlanner(t *testing.T) { +// +// // make and configure a mocked Planner +// mockedPlanner := &PlannerMock{ +// HealthFunc: func(ctx context.Context) error { +// panic("mock out the Health method") +// }, +// UpdateAgentStatusFunc: func(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error { +// panic("mock out the UpdateAgentStatus method") +// }, +// UpdateSourceStatusFunc: func(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error { +// panic("mock out the UpdateSourceStatus method") +// }, +// } +// +// // use mockedPlanner in code that requires Planner +// // and then make assertions. +// +// } +type PlannerMock struct { + // HealthFunc mocks the Health method. + HealthFunc func(ctx context.Context) error + + // UpdateAgentStatusFunc mocks the UpdateAgentStatus method. + UpdateAgentStatusFunc func(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error + + // UpdateSourceStatusFunc mocks the UpdateSourceStatus method. + UpdateSourceStatusFunc func(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error + + // calls tracks calls to the methods. + calls struct { + // Health holds details about calls to the Health method. + Health []struct { + // Ctx is the ctx argument value. + Ctx context.Context + } + // UpdateAgentStatus holds details about calls to the UpdateAgentStatus method. + UpdateAgentStatus []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // ID is the id argument value. + ID uuid.UUID + // Params is the params argument value. + Params api.AgentStatusUpdate + } + // UpdateSourceStatus holds details about calls to the UpdateSourceStatus method. + UpdateSourceStatus []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // ID is the id argument value. + ID uuid.UUID + // Params is the params argument value. + Params api.SourceStatusUpdate + } + } + lockHealth sync.RWMutex + lockUpdateAgentStatus sync.RWMutex + lockUpdateSourceStatus sync.RWMutex +} + +// Health calls HealthFunc. +func (mock *PlannerMock) Health(ctx context.Context) error { + if mock.HealthFunc == nil { + panic("PlannerMock.HealthFunc: method is nil but Planner.Health was just called") + } + callInfo := struct { + Ctx context.Context + }{ + Ctx: ctx, + } + mock.lockHealth.Lock() + mock.calls.Health = append(mock.calls.Health, callInfo) + mock.lockHealth.Unlock() + return mock.HealthFunc(ctx) +} + +// HealthCalls gets all the calls that were made to Health. +// Check the length with: +// +// len(mockedPlanner.HealthCalls()) +func (mock *PlannerMock) HealthCalls() []struct { + Ctx context.Context +} { + var calls []struct { + Ctx context.Context + } + mock.lockHealth.RLock() + calls = mock.calls.Health + mock.lockHealth.RUnlock() + return calls +} + +// UpdateAgentStatus calls UpdateAgentStatusFunc. +func (mock *PlannerMock) UpdateAgentStatus(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error { + if mock.UpdateAgentStatusFunc == nil { + panic("PlannerMock.UpdateAgentStatusFunc: method is nil but Planner.UpdateAgentStatus was just called") + } + callInfo := struct { + Ctx context.Context + ID uuid.UUID + Params api.AgentStatusUpdate + }{ + Ctx: ctx, + ID: id, + Params: params, + } + mock.lockUpdateAgentStatus.Lock() + mock.calls.UpdateAgentStatus = append(mock.calls.UpdateAgentStatus, callInfo) + mock.lockUpdateAgentStatus.Unlock() + return mock.UpdateAgentStatusFunc(ctx, id, params) +} + +// UpdateAgentStatusCalls gets all the calls that were made to UpdateAgentStatus. +// Check the length with: +// +// len(mockedPlanner.UpdateAgentStatusCalls()) +func (mock *PlannerMock) UpdateAgentStatusCalls() []struct { + Ctx context.Context + ID uuid.UUID + Params api.AgentStatusUpdate +} { + var calls []struct { + Ctx context.Context + ID uuid.UUID + Params api.AgentStatusUpdate + } + mock.lockUpdateAgentStatus.RLock() + calls = mock.calls.UpdateAgentStatus + mock.lockUpdateAgentStatus.RUnlock() + return calls +} + +// UpdateSourceStatus calls UpdateSourceStatusFunc. +func (mock *PlannerMock) UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error { + if mock.UpdateSourceStatusFunc == nil { + panic("PlannerMock.UpdateSourceStatusFunc: method is nil but Planner.UpdateSourceStatus was just called") + } + callInfo := struct { + Ctx context.Context + ID uuid.UUID + Params api.SourceStatusUpdate + }{ + Ctx: ctx, + ID: id, + Params: params, + } + mock.lockUpdateSourceStatus.Lock() + mock.calls.UpdateSourceStatus = append(mock.calls.UpdateSourceStatus, callInfo) + mock.lockUpdateSourceStatus.Unlock() + return mock.UpdateSourceStatusFunc(ctx, id, params) +} + +// UpdateSourceStatusCalls gets all the calls that were made to UpdateSourceStatus. +// Check the length with: +// +// len(mockedPlanner.UpdateSourceStatusCalls()) +func (mock *PlannerMock) UpdateSourceStatusCalls() []struct { + Ctx context.Context + ID uuid.UUID + Params api.SourceStatusUpdate +} { + var calls []struct { + Ctx context.Context + ID uuid.UUID + Params api.SourceStatusUpdate + } + mock.lockUpdateSourceStatus.RLock() + calls = mock.calls.UpdateSourceStatus + mock.lockUpdateSourceStatus.RUnlock() + return calls +} diff --git a/internal/agent/health_test.go b/internal/agent/health_test.go index e92fee8..fd55580 100644 --- a/internal/agent/health_test.go +++ b/internal/agent/health_test.go @@ -10,7 +10,6 @@ import ( "github.com/google/uuid" api "github.com/kubev2v/migration-planner/api/v1alpha1/agent" - client "github.com/kubev2v/migration-planner/internal/api/client/agent" "github.com/kubev2v/migration-planner/pkg/log" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -232,6 +231,10 @@ func (c *agentTestClient) Health(ctx context.Context) error { return nil } -func (c *agentTestClient) UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate, rcb ...client.RequestEditorFn) error { +func (c *agentTestClient) UpdateSourceStatus(ctx context.Context, id uuid.UUID, params api.SourceStatusUpdate) error { + return nil +} + +func (c *agentTestClient) UpdateAgentStatus(ctx context.Context, id uuid.UUID, params api.AgentStatusUpdate) error { return nil } diff --git a/internal/agent/inventory.go b/internal/agent/inventory.go index bf7299d..f48a587 100644 --- a/internal/agent/inventory.go +++ b/internal/agent/inventory.go @@ -4,22 +4,19 @@ import ( "bytes" "context" "encoding/json" - "fmt" - "path/filepath" "github.com/google/uuid" api "github.com/kubev2v/migration-planner/api/v1alpha1" agentapi "github.com/kubev2v/migration-planner/api/v1alpha1/agent" "github.com/kubev2v/migration-planner/internal/agent/client" - "github.com/kubev2v/migration-planner/internal/agent/fileio" "github.com/kubev2v/migration-planner/pkg/log" ) type InventoryUpdater struct { log *log.PrefixLogger - config *Config + sourceID uuid.UUID client client.Planner - credUrl string + agentID uuid.UUID prevStatus []byte } @@ -28,29 +25,22 @@ type InventoryData struct { Error string `json:"error"` } -func NewInventoryUpdater(log *log.PrefixLogger, config *Config, credUrl string, client client.Planner) *InventoryUpdater { - return &InventoryUpdater{ +func NewInventoryUpdater(log *log.PrefixLogger, agentID uuid.UUID, client client.Planner) *InventoryUpdater { + updater := &InventoryUpdater{ log: log, - config: config, client: client, + agentID: agentID, prevStatus: []byte{}, - credUrl: credUrl, } + return updater } -func (u *InventoryUpdater) UpdateServiceWithInventory(ctx context.Context) { - status, statusInfo, inventory := calculateStatus(u.config.DataDir) - u.updateSourceStatus(ctx, status, statusInfo, inventory) -} - -func (u *InventoryUpdater) updateSourceStatus(ctx context.Context, status api.SourceStatus, statusInfo string, inventory *api.Inventory) { +func (u *InventoryUpdater) UpdateServiceWithInventory(ctx context.Context, status api.SourceStatus, statusInfo string, inventory *api.Inventory) { update := agentapi.SourceStatusUpdate{ - Status: string(status), - StatusInfo: statusInfo, - Inventory: inventory, - CredentialUrl: u.credUrl, - // TODO: when moving to AgentStatusUpdate put this: - //Version: version, + Status: string(status), + StatusInfo: statusInfo, + Inventory: inventory, + AgentId: u.agentID, } newContents, err := json.Marshal(update) @@ -63,7 +53,7 @@ func (u *InventoryUpdater) updateSourceStatus(ctx context.Context, status api.So } u.log.Debugf("Updating status to %s: %s", string(status), statusInfo) - err = u.client.UpdateSourceStatus(ctx, uuid.MustParse(u.config.SourceID), update) + err = u.client.UpdateSourceStatus(ctx, uuid.MustParse(inventory.Vcenter.Id), update) if err != nil { u.log.Errorf("failed updating status: %v", err) return @@ -71,31 +61,3 @@ func (u *InventoryUpdater) updateSourceStatus(ctx context.Context, status api.So u.prevStatus = newContents } - -func calculateStatus(dataDir string) (api.SourceStatus, string, *api.Inventory) { - inventoryFilePath := filepath.Join(dataDir, InventoryFile) - credentialsFilePath := filepath.Join(dataDir, CredentialsFile) - reader := fileio.NewReader() - - err := reader.CheckPathExists(credentialsFilePath) - if err != nil { - return api.SourceStatusWaitingForCredentials, "No credentials provided", nil - } - err = reader.CheckPathExists(inventoryFilePath) - if err != nil { - return api.SourceStatusGatheringInitialInventory, "Inventory not yet collected", nil - } - inventoryData, err := reader.ReadFile(inventoryFilePath) - if err != nil { - return api.SourceStatusError, fmt.Sprintf("Failed reading inventory file: %v", err), nil - } - var inventory InventoryData - err = json.Unmarshal(inventoryData, &inventory) - if err != nil { - return api.SourceStatusError, fmt.Sprintf("Invalid inventory file: %v", err), nil - } - if len(inventory.Error) > 0 { - return api.SourceStatusError, inventory.Error, &inventory.Inventory - } - return api.SourceStatusUpToDate, "Inventory successfully collected", &inventory.Inventory -} diff --git a/internal/agent/inventory_test.go b/internal/agent/inventory_test.go new file mode 100644 index 0000000..7a1af02 --- /dev/null +++ b/internal/agent/inventory_test.go @@ -0,0 +1,47 @@ +package agent_test + +import ( + "context" + + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1" + v1alpha1 "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "github.com/kubev2v/migration-planner/internal/agent" + "github.com/kubev2v/migration-planner/internal/agent/client" + "github.com/kubev2v/migration-planner/pkg/log" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Inventory", func() { + var ( + agentID uuid.UUID + sourceID uuid.UUID + ) + BeforeEach(func() { + agentID, _ = uuid.NewUUID() + sourceID, _ = uuid.NewUUID() + }) + + Context("update inventory", func() { + It("successfully updates inventory", func() { + client := client.PlannerMock{ + UpdateSourceStatusFunc: func(ctx context.Context, id uuid.UUID, params v1alpha1.SourceStatusUpdate) error { + Expect(id).To(Equal(sourceID)) + Expect(params.AgentId).To(Equal(agentID)) + Expect(params.Status).To(Equal("up-to-date")) + Expect(params.StatusInfo).To(Equal("status_info")) + Expect(params.Inventory).ToNot(BeNil()) + return nil + + }, + } + + inventory := &api.Inventory{ + Vms: api.VMs{Total: 2}, + } + inventoryUpdater := agent.NewInventoryUpdater(log.NewPrefixLogger(""), agentID, &client) + inventoryUpdater.UpdateServiceWithInventory(context.TODO(), sourceID, api.SourceStatusUpToDate, "status_info", inventory) + }) + }) +}) diff --git a/internal/agent/rest.go b/internal/agent/rest.go index bee4dc5..6659b7f 100644 --- a/internal/agent/rest.go +++ b/internal/agent/rest.go @@ -26,12 +26,13 @@ const ( CredentialsFile = "credentials.json" ) -func RegisterApi(router *chi.Mux, log *log.PrefixLogger, dataDir string) { +func RegisterApi(router *chi.Mux, log *log.PrefixLogger, statusUpdater *StatusUpdater, dataDir string) { router.Get("/api/v1/version", func(w http.ResponseWriter, r *http.Request) { _ = render.Render(w, r, VersionReply{Version: version}) }) router.Get("/api/v1/status", func(w http.ResponseWriter, r *http.Request) { - statusHandler(dataDir, w, r) + status, statusInfo, _ := statusUpdater.CalculateStatus() + _ = render.Render(w, r, StatusReply{Status: string(status), StatusInfo: statusInfo}) }) router.Put("/api/v1/credentials", func(w http.ResponseWriter, r *http.Request) { credentialHandler(log, dataDir, w, r) @@ -55,11 +56,6 @@ func (v VersionReply) Render(w http.ResponseWriter, r *http.Request) error { return nil } -func statusHandler(dataDir string, w http.ResponseWriter, r *http.Request) { - status, statusInfo, _ := calculateStatus(dataDir) - _ = render.Render(w, r, StatusReply{Status: string(status), StatusInfo: statusInfo}) -} - type Credentials struct { URL string `json:"url"` Username string `json:"username"` diff --git a/internal/agent/server.go b/internal/agent/server.go index 5401ebe..e9fd52c 100644 --- a/internal/agent/server.go +++ b/internal/agent/server.go @@ -33,13 +33,13 @@ func NewServer(port int, dataFolder, wwwFolder string) *Server { } } -func (s *Server) Start(log *log.PrefixLogger) { +func (s *Server) Start(log *log.PrefixLogger, statusUpdater *StatusUpdater) { router := chi.NewRouter() router.Use(middleware.RequestID) router.Use(middleware.Logger) RegisterFileServer(router, log, s.wwwFolder) - RegisterApi(router, log, s.dataFolder) + RegisterApi(router, log, statusUpdater, s.dataFolder) s.restServer = &http.Server{Addr: fmt.Sprintf("0.0.0.0:%d", s.port), Handler: router} diff --git a/internal/agent/status.go b/internal/agent/status.go new file mode 100644 index 0000000..5a2fa25 --- /dev/null +++ b/internal/agent/status.go @@ -0,0 +1,83 @@ +package agent + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + "time" + + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1" + agentapi "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "github.com/kubev2v/migration-planner/internal/agent/client" + "github.com/kubev2v/migration-planner/internal/agent/fileio" + "github.com/kubev2v/migration-planner/pkg/log" +) + +const ( + defaultUpdateStatusTimeout = 5 * time.Second +) + +type StatusUpdater struct { + agentID uuid.UUID + log *log.PrefixLogger + version string + config *Config + client client.Planner + credUrl string +} + +func NewStatusUpdater(log *log.PrefixLogger, agentID uuid.UUID, version, credUrl string, config *Config, client client.Planner) *StatusUpdater { + return &StatusUpdater{ + log: log, + client: client, + config: config, + agentID: agentID, + credUrl: credUrl, + version: version, + } +} + +func (s *StatusUpdater) UpdateStatus(ctx context.Context, status api.AgentStatus, statusInfo string) error { + ctx, cancel := context.WithTimeout(ctx, defaultUpdateStatusTimeout*time.Second) + defer cancel() + + bodyParameters := agentapi.AgentStatusUpdate{ + Id: s.agentID.String(), + Status: string(status), + StatusInfo: statusInfo, + CredentialUrl: s.credUrl, + Version: s.version, + } + + return s.client.UpdateAgentStatus(ctx, s.agentID, bodyParameters) +} + +func (s *StatusUpdater) CalculateStatus() (api.AgentStatus, string, *api.Inventory) { + inventoryFilePath := filepath.Join(s.config.DataDir, InventoryFile) + credentialsFilePath := filepath.Join(s.config.DataDir, CredentialsFile) + reader := fileio.NewReader() + + err := reader.CheckPathExists(credentialsFilePath) + if err != nil { + return api.AgentStatusWaitingForCredentials, "No credentials provided", nil + } + err = reader.CheckPathExists(inventoryFilePath) + if err != nil { + return api.AgentStatusGatheringInitialInventory, "Inventory not yet collected", nil + } + inventoryData, err := reader.ReadFile(inventoryFilePath) + if err != nil { + return api.AgentStatusError, fmt.Sprintf("Failed reading inventory file: %v", err), nil + } + var inventory InventoryData + err = json.Unmarshal(inventoryData, &inventory) + if err != nil { + return api.AgentStatusError, fmt.Sprintf("Invalid inventory file: %v", err), nil + } + if len(inventory.Error) > 0 { + return api.AgentStatusError, inventory.Error, &inventory.Inventory + } + return api.AgentStatusUpToDate, "Inventory successfully collected", &inventory.Inventory +} diff --git a/internal/agent/status_test.go b/internal/agent/status_test.go new file mode 100644 index 0000000..78b282c --- /dev/null +++ b/internal/agent/status_test.go @@ -0,0 +1,131 @@ +package agent_test + +import ( + "context" + "os" + "path" + + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1" + v1alpha1 "github.com/kubev2v/migration-planner/api/v1alpha1/agent" + "github.com/kubev2v/migration-planner/internal/agent" + "github.com/kubev2v/migration-planner/internal/agent/client" + "github.com/kubev2v/migration-planner/pkg/log" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + noCredentialsProvidedStatusInfo = "No credentials provided" + inventoryNotYetCollectedStatusInfo = "Inventory not yet collected" + inventoryCollectedStatusInfo = "Inventory successfully collected" +) + +var _ = Describe("Status", func() { + var agentID uuid.UUID + BeforeEach(func() { + agentID, _ = uuid.NewUUID() + }) + + Context("update status", func() { + It("successfully updates status", func() { + client := client.PlannerMock{ + UpdateAgentStatusFunc: func(ctx context.Context, id uuid.UUID, params v1alpha1.AgentStatusUpdate) error { + Expect(id).To(Equal(agentID)) + Expect(params.Version).To(Equal("best_version")) + Expect(params.Status).To(Equal("up-to-date")) + Expect(params.CredentialUrl).To(Equal("www-cred-url")) + Expect(params.StatusInfo).To(Equal("status_info")) + return nil + }, + } + + statusUpdater := agent.NewStatusUpdater(log.NewPrefixLogger(""), agentID, "best_version", "www-cred-url", &agent.Config{}, &client) + Expect(statusUpdater.UpdateStatus(context.TODO(), api.AgentStatusUpToDate, "status_info")) + }) + }) + + Context("compute status", func() { + var ( + dataTmpFolder string + plannerClient *client.PlannerMock + ) + BeforeEach(func() { + var err error + dataTmpFolder, err = os.MkdirTemp("", "agent-data-folder") + Expect(err).To(BeNil()) + plannerClient = &client.PlannerMock{} + }) + AfterEach(func() { + os.RemoveAll(dataTmpFolder) + }) + + It("compute status returns Waiting for credentials", func() { + statusUpdater := agent.NewStatusUpdater(log.NewPrefixLogger(""), + agentID, + "best_version", + "www-cred-url", + &agent.Config{ + DataDir: dataTmpFolder, + }, + plannerClient, + ) + + status, status_info, inventory := statusUpdater.CalculateStatus() + Expect(status).To(Equal(api.AgentStatusWaitingForCredentials)) + Expect(status_info).To(Equal(noCredentialsProvidedStatusInfo)) + Expect(inventory).To(BeNil()) + }) + + It("compute status returns GatheringInitialInventory", func() { + // create credentials.json + creds, err := os.Create(path.Join(dataTmpFolder, "credentials.json")) + Expect(err).To(BeNil()) + creds.Close() + + statusUpdater := agent.NewStatusUpdater(log.NewPrefixLogger(""), + agentID, + "best_version", + "www-cred-url", + &agent.Config{ + DataDir: dataTmpFolder, + }, + plannerClient, + ) + + status, status_info, inventory := statusUpdater.CalculateStatus() + Expect(status).To(Equal(api.AgentStatusGatheringInitialInventory)) + Expect(status_info).To(Equal(inventoryNotYetCollectedStatusInfo)) + Expect(inventory).To(BeNil()) + }) + + It("compute status returns InventoryUptoDate", func() { + // create credentials.json + creds, err := os.Create(path.Join(dataTmpFolder, "credentials.json")) + Expect(err).To(BeNil()) + creds.Close() + + inventoryFile, err := os.Create(path.Join(dataTmpFolder, "inventory.json")) + Expect(err).To(BeNil()) + + _, err = inventoryFile.Write([]byte("{\"inventory\": {}, \"error\": \"\"}")) + Expect(err).To(BeNil()) + + statusUpdater := agent.NewStatusUpdater(log.NewPrefixLogger(""), + agentID, + "best_version", + "www-cred-url", + &agent.Config{ + DataDir: dataTmpFolder, + }, + plannerClient, + ) + + status, status_info, inventory := statusUpdater.CalculateStatus() + Expect(status).To(Equal(api.AgentStatusUpToDate)) + Expect(status_info).To(Equal(inventoryCollectedStatusInfo)) + Expect(inventory).ToNot(BeNil()) + }) + }) + +}) diff --git a/internal/service/agent/handler.go b/internal/service/agent/handler.go index a61d7a4..36cd958 100644 --- a/internal/service/agent/handler.go +++ b/internal/service/agent/handler.go @@ -24,7 +24,7 @@ func NewAgentServiceHandler(store store.Store, log logrus.FieldLogger) *AgentSer } func (h *AgentServiceHandler) ReplaceSourceStatus(ctx context.Context, request agentServer.ReplaceSourceStatusRequestObject) (agentServer.ReplaceSourceStatusResponseObject, error) { - result, err := h.store.Source().Update(ctx, request.Id, &request.Body.Status, &request.Body.StatusInfo, &request.Body.CredentialUrl, request.Body.Inventory) + result, err := h.store.Source().Update(ctx, request.Id, &request.Body.Status, &request.Body.StatusInfo, request.Body.CredentialUrl, request.Body.Inventory) if err != nil { return agentServer.ReplaceSourceStatus400JSONResponse{}, nil }