THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

This is Gitea test Portainer repository mirror from Github
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

314 lines
11 KiB

  1. package exec
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "log"
  8. "os"
  9. "os/exec"
  10. "path"
  11. "regexp"
  12. "runtime"
  13. "strconv"
  14. "strings"
  15. "time"
  16. "github.com/coreos/go-semver/semver"
  17. "github.com/orcaman/concurrent-map"
  18. "github.com/portainer/portainer/api"
  19. "github.com/portainer/portainer/api/http/client"
  20. )
  21. var extensionDownloadBaseURL = portainer.AssetsServerURL + "/extensions/"
  22. var extensionVersionRegexp = regexp.MustCompile(`\d+(\.\d+)+`)
  23. var extensionBinaryMap = map[portainer.ExtensionID]string{
  24. portainer.RegistryManagementExtension: "extension-registry-management",
  25. portainer.OAuthAuthenticationExtension: "extension-oauth-authentication",
  26. portainer.RBACExtension: "extension-rbac",
  27. }
  28. // ExtensionManager represents a service used to
  29. // manage extension processes.
  30. type ExtensionManager struct {
  31. processes cmap.ConcurrentMap
  32. fileService portainer.FileService
  33. extensionService portainer.ExtensionService
  34. }
  35. // NewExtensionManager returns a pointer to an ExtensionManager
  36. func NewExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) *ExtensionManager {
  37. return &ExtensionManager{
  38. processes: cmap.New(),
  39. fileService: fileService,
  40. extensionService: extensionService,
  41. }
  42. }
  43. func processKey(ID portainer.ExtensionID) string {
  44. return strconv.Itoa(int(ID))
  45. }
  46. func buildExtensionURL(extension *portainer.Extension) string {
  47. return fmt.Sprintf("%s%s-%s-%s-%s.zip", extensionDownloadBaseURL, extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
  48. }
  49. func buildExtensionPath(binaryPath string, extension *portainer.Extension) string {
  50. extensionFilename := fmt.Sprintf("%s-%s-%s-%s", extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
  51. if runtime.GOOS == "windows" {
  52. extensionFilename += ".exe"
  53. }
  54. extensionPath := path.Join(
  55. binaryPath,
  56. extensionFilename)
  57. return extensionPath
  58. }
  59. // FetchExtensionDefinitions will fetch the list of available
  60. // extension definitions from the official Portainer assets server.
  61. // If it cannot retrieve the data from the Internet it will fallback to the locally cached
  62. // manifest file.
  63. func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extension, error) {
  64. var extensionData []byte
  65. extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 5)
  66. if err != nil {
  67. log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extensions manifest via Internet. Extensions will be retrieved from local cache and might not be up to date] [err: %s]", err)
  68. extensionData, err = manager.fileService.GetFileContent(portainer.LocalExtensionManifestFile)
  69. if err != nil {
  70. return nil, err
  71. }
  72. }
  73. var extensions []portainer.Extension
  74. err = json.Unmarshal(extensionData, &extensions)
  75. if err != nil {
  76. return nil, err
  77. }
  78. return extensions, nil
  79. }
  80. // InstallExtension will install the extension from an archive. It will extract the extension version number from
  81. // the archive file name first and return an error if the file name is not valid (cannot find extension version).
  82. // It will then extract the archive and execute the EnableExtension function to enable the extension.
  83. // Since we're missing information about this extension (stored on Portainer.io server) we need to assume
  84. // default information based on the extension ID.
  85. func (manager *ExtensionManager) InstallExtension(extension *portainer.Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error {
  86. extensionVersion := extensionVersionRegexp.FindString(archiveFileName)
  87. if extensionVersion == "" {
  88. return errors.New("invalid extension archive filename: unable to retrieve extension version")
  89. }
  90. err := manager.fileService.ExtractExtensionArchive(extensionArchive)
  91. if err != nil {
  92. return err
  93. }
  94. switch extension.ID {
  95. case portainer.RegistryManagementExtension:
  96. extension.Name = "Registry Manager"
  97. case portainer.OAuthAuthenticationExtension:
  98. extension.Name = "External Authentication"
  99. case portainer.RBACExtension:
  100. extension.Name = "Role-Based Access Control"
  101. }
  102. extension.ShortDescription = "Extension enabled offline"
  103. extension.Version = extensionVersion
  104. extension.Available = true
  105. return manager.EnableExtension(extension, licenseKey)
  106. }
  107. // EnableExtension will check for the existence of the extension binary on the filesystem
  108. // first. If it does not exist, it will download it from the official Portainer assets server.
  109. // After installing the binary on the filesystem, it will execute the binary in license check
  110. // mode to validate the extension license. If the license is valid, it will then start
  111. // the extension process and register it in the processes map.
  112. func (manager *ExtensionManager) EnableExtension(extension *portainer.Extension, licenseKey string) error {
  113. extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
  114. extensionBinaryExists, err := manager.fileService.FileExists(extensionBinaryPath)
  115. if err != nil {
  116. return err
  117. }
  118. if !extensionBinaryExists {
  119. err := manager.downloadExtension(extension)
  120. if err != nil {
  121. return err
  122. }
  123. }
  124. licenseDetails, err := validateLicense(extensionBinaryPath, licenseKey)
  125. if err != nil {
  126. return err
  127. }
  128. extension.License = portainer.LicenseInformation{
  129. LicenseKey: licenseKey,
  130. Company: licenseDetails[0],
  131. Expiration: licenseDetails[1],
  132. Valid: true,
  133. }
  134. extension.Version = licenseDetails[2]
  135. return manager.startExtensionProcess(extension, extensionBinaryPath)
  136. }
  137. // DisableExtension will retrieve the process associated to the extension
  138. // from the processes map and kill the process. It will then remove the process
  139. // from the processes map and remove the binary associated to the extension
  140. // from the filesystem
  141. func (manager *ExtensionManager) DisableExtension(extension *portainer.Extension) error {
  142. process, ok := manager.processes.Get(processKey(extension.ID))
  143. if !ok {
  144. return nil
  145. }
  146. err := process.(*exec.Cmd).Process.Kill()
  147. if err != nil {
  148. return err
  149. }
  150. manager.processes.Remove(processKey(extension.ID))
  151. extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
  152. return manager.fileService.RemoveDirectory(extensionBinaryPath)
  153. }
  154. // StartExtensions will retrieve the extensions definitions from the Internet and check if a new version of each
  155. // extension is available. If so, it will automatically install the new version of the extension. If no update is
  156. // available it will simply start the extension.
  157. // The purpose of this function is to be ran at startup, as such most of the error handling won't block the program execution
  158. // and will log warning messages instead.
  159. func (manager *ExtensionManager) StartExtensions() error {
  160. extensions, err := manager.extensionService.Extensions()
  161. if err != nil {
  162. return err
  163. }
  164. definitions, err := manager.FetchExtensionDefinitions()
  165. if err != nil {
  166. log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extension information from Internet. Skipping extensions update check.] [err: %s]", err)
  167. return nil
  168. }
  169. return manager.updateAndStartExtensions(extensions, definitions)
  170. }
  171. func (manager *ExtensionManager) updateAndStartExtensions(extensions []portainer.Extension, definitions []portainer.Extension) error {
  172. for _, definition := range definitions {
  173. for _, extension := range extensions {
  174. if extension.ID == definition.ID {
  175. definitionVersion := semver.New(definition.Version)
  176. extensionVersion := semver.New(extension.Version)
  177. if extensionVersion.LessThan(*definitionVersion) {
  178. log.Printf("[INFO] [exec,extensions] [message: new version detected, updating extension] [extension: %s] [current_version: %s] [available_version: %s]", extension.Name, extension.Version, definition.Version)
  179. err := manager.UpdateExtension(&extension, definition.Version)
  180. if err != nil {
  181. log.Printf("[WARN] [exec,extensions] [message: unable to update extension automatically] [extension: %s] [current_version: %s] [available_version: %s] [err: %s]", extension.Name, extension.Version, definition.Version, err)
  182. }
  183. } else {
  184. err := manager.EnableExtension(&extension, extension.License.LicenseKey)
  185. if err != nil {
  186. log.Printf("[WARN] [exec,extensions] [message: unable to start extension] [extension: %s] [err: %s]", extension.Name, err)
  187. extension.Enabled = false
  188. extension.License.Valid = false
  189. }
  190. }
  191. err := manager.extensionService.Persist(&extension)
  192. if err != nil {
  193. return err
  194. }
  195. break
  196. }
  197. }
  198. }
  199. return nil
  200. }
  201. // UpdateExtension will download the new extension binary from the official Portainer assets
  202. // server, disable the previous extension via DisableExtension, trigger a license check
  203. // and then start the extension process and add it to the processes map
  204. func (manager *ExtensionManager) UpdateExtension(extension *portainer.Extension, version string) error {
  205. oldVersion := extension.Version
  206. extension.Version = version
  207. err := manager.downloadExtension(extension)
  208. if err != nil {
  209. return err
  210. }
  211. extension.Version = oldVersion
  212. err = manager.DisableExtension(extension)
  213. if err != nil {
  214. return err
  215. }
  216. extension.Version = version
  217. extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
  218. licenseDetails, err := validateLicense(extensionBinaryPath, extension.License.LicenseKey)
  219. if err != nil {
  220. return err
  221. }
  222. extension.Version = licenseDetails[2]
  223. return manager.startExtensionProcess(extension, extensionBinaryPath)
  224. }
  225. func (manager *ExtensionManager) downloadExtension(extension *portainer.Extension) error {
  226. extensionURL := buildExtensionURL(extension)
  227. data, err := client.Get(extensionURL, 30)
  228. if err != nil {
  229. return err
  230. }
  231. return manager.fileService.ExtractExtensionArchive(data)
  232. }
  233. func validateLicense(binaryPath, licenseKey string) ([]string, error) {
  234. licenseCheckProcess := exec.Command(binaryPath, "-license", licenseKey, "-check")
  235. cmdOutput := &bytes.Buffer{}
  236. licenseCheckProcess.Stdout = cmdOutput
  237. err := licenseCheckProcess.Run()
  238. if err != nil {
  239. log.Printf("[DEBUG] [exec,extension] [message: unable to run extension process] [err: %s]", err)
  240. return nil, errors.New("invalid extension license key")
  241. }
  242. output := string(cmdOutput.Bytes())
  243. return strings.Split(output, "|"), nil
  244. }
  245. func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Extension, binaryPath string) error {
  246. extensionProcess := exec.Command(binaryPath, "-license", extension.License.LicenseKey)
  247. extensionProcess.Stdout = os.Stdout
  248. extensionProcess.Stderr = os.Stderr
  249. err := extensionProcess.Start()
  250. if err != nil {
  251. log.Printf("[DEBUG] [exec,extension] [message: unable to start extension process] [err: %s]", err)
  252. return err
  253. }
  254. time.Sleep(3 * time.Second)
  255. manager.processes.Set(processKey(extension.ID), extensionProcess)
  256. return nil
  257. }