Açıklama Yok

docker.go 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  1. package main
  2. // Docker
  3. import (
  4. "archive/tar"
  5. "github.com/shuffle/shuffle-shared"
  6. //"bufio"
  7. "path/filepath"
  8. //"strconv"
  9. "bytes"
  10. "context"
  11. "encoding/base64"
  12. "encoding/json"
  13. "errors"
  14. "fmt"
  15. //"github.com/docker/docker"
  16. "github.com/docker/docker/api/types"
  17. //"github.com/docker/docker/api/types/container"
  18. "github.com/docker/docker/api/types/image"
  19. newdockerclient "github.com/fsouza/go-dockerclient"
  20. "github.com/go-git/go-billy/v5"
  21. //network "github.com/docker/docker/api/types/network"
  22. //natting "github.com/docker/go-connections/nat"
  23. "io"
  24. "io/ioutil"
  25. "log"
  26. "net/http"
  27. "os"
  28. "strings"
  29. "time"
  30. batchv1 "k8s.io/api/batch/v1"
  31. corev1 "k8s.io/api/core/v1"
  32. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  33. "k8s.io/client-go/kubernetes"
  34. "k8s.io/client-go/rest"
  35. // "k8s.io/client-go/tools/clientcmd"
  36. // "k8s.io/client-go/util/homedir"
  37. )
  38. // Parses a directory with a Dockerfile into a tar for Docker images..
  39. func getParsedTar(tw *tar.Writer, baseDir, extra string) error {
  40. return filepath.Walk(baseDir, func(file string, fi os.FileInfo, err error) error {
  41. if file == baseDir {
  42. return nil
  43. }
  44. //log.Printf("File: %s", file)
  45. //log.Printf("Fileinfo: %#v", fi)
  46. switch mode := fi.Mode(); {
  47. case mode.IsDir():
  48. // do directory recursion
  49. // Cross-platform path handling for tar entries
  50. filenamesplit := strings.Split(filepath.ToSlash(file), "/")
  51. filename := fmt.Sprintf("%s%s/", extra, filenamesplit[len(filenamesplit)-1])
  52. tmpExtra := fmt.Sprintf(filename)
  53. err = getParsedTar(tw, file, tmpExtra)
  54. if err != nil {
  55. log.Printf("Directory parse issue: %s", err)
  56. return err
  57. }
  58. case mode.IsRegular():
  59. // do file stuff
  60. //log.Printf("FILE: %s", file)
  61. fileReader, err := os.Open(file)
  62. if err != nil {
  63. return err
  64. }
  65. // Read the actual Dockerfile
  66. readFile, err := ioutil.ReadAll(fileReader)
  67. if err != nil {
  68. log.Printf("Not file: %s", err)
  69. return err
  70. }
  71. filenamesplit := strings.Split(filepath.ToSlash(file), "/")
  72. filename := fmt.Sprintf("%s%s", extra, filenamesplit[len(filenamesplit)-1])
  73. tarHeader := &tar.Header{
  74. Name: filename,
  75. Size: int64(len(readFile)),
  76. }
  77. //Writes the header described for the TAR file
  78. err = tw.WriteHeader(tarHeader)
  79. if err != nil {
  80. return err
  81. }
  82. // Writes the dockerfile data to the TAR file
  83. _, err = tw.Write(readFile)
  84. if err != nil {
  85. return err
  86. }
  87. }
  88. return nil
  89. })
  90. }
  91. // Custom TAR builder in memory for Docker images
  92. func getParsedTarMemory(fs billy.Filesystem, tw *tar.Writer, baseDir, extra string) error {
  93. // This one has to use baseDir + Extra
  94. newBase := fmt.Sprintf("%s%s", baseDir, extra)
  95. dir, err := fs.ReadDir(newBase)
  96. if err != nil {
  97. return err
  98. }
  99. for _, file := range dir {
  100. // Folder?
  101. switch mode := file.Mode(); {
  102. case mode.IsDir():
  103. filename := file.Name()
  104. filenamesplit := strings.Split(filename, "/")
  105. tmpExtra := fmt.Sprintf("%s%s/", extra, filenamesplit[len(filenamesplit)-1])
  106. //log.Printf("EXTRA: %s", tmpExtra)
  107. err = getParsedTarMemory(fs, tw, baseDir, tmpExtra)
  108. if err != nil {
  109. log.Printf("Directory parse issue: %s", err)
  110. return err
  111. }
  112. case mode.IsRegular():
  113. filenamesplit := strings.Split(file.Name(), "/")
  114. filename := fmt.Sprintf("%s%s", extra, filenamesplit[len(filenamesplit)-1])
  115. // Newbase
  116. path := fmt.Sprintf("%s%s", newBase, file.Name())
  117. fileReader, err := fs.Open(path)
  118. if err != nil {
  119. return err
  120. }
  121. //log.Printf("FILENAME: %s", filename)
  122. readFile, err := ioutil.ReadAll(fileReader)
  123. if err != nil {
  124. log.Printf("Not file: %s", err)
  125. return err
  126. }
  127. // Fixes issues with older versions of Docker and reference formats
  128. // Specific to Shuffle rn. Could expand.
  129. // FIXME: Seems like the issue was with multi-stage builds
  130. /*
  131. if filename == "Dockerfile" {
  132. log.Printf("Should search and replace in readfile.")
  133. referenceCheck := "FROM frikky/shuffle:"
  134. if strings.Contains(string(readFile), referenceCheck) {
  135. log.Printf("SHOULD SEARCH & REPLACE!")
  136. newReference := fmt.Sprintf("FROM registry.hub.docker.com/frikky/shuffle:")
  137. readFile = []byte(strings.Replace(string(readFile), referenceCheck, newReference, -1))
  138. }
  139. }
  140. */
  141. //log.Printf("Filename: %s", filename)
  142. // FIXME - might need the folder from EXTRA here
  143. // Name has to be e.g. just "requirements.txt"
  144. tarHeader := &tar.Header{
  145. Name: filename,
  146. Size: int64(len(readFile)),
  147. }
  148. //Writes the header described for the TAR file
  149. err = tw.WriteHeader(tarHeader)
  150. if err != nil {
  151. return err
  152. }
  153. // Writes the dockerfile data to the TAR file
  154. _, err = tw.Write(readFile)
  155. if err != nil {
  156. return err
  157. }
  158. }
  159. }
  160. return nil
  161. }
  162. /*
  163. // Fixes App SDK issues.. meh
  164. func fixTags(tags []string) []string {
  165. checkTag := "frikky/shuffle"
  166. newTags := []string{}
  167. for _, tag := range tags {
  168. if strings.HasPrefix(tag, checkTags) {
  169. newTags.append(newTags, fmt.Sprintf("registry.hub.docker.com/%s", tag))
  170. }
  171. newTags.append(tag)
  172. }
  173. }
  174. */
  175. // Custom Docker image builder wrapper in memory
  176. func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder string, downloadIfFail bool) error {
  177. ctx := context.Background()
  178. // client, err := client.NewEnvClient()
  179. client, _, err := shuffle.GetDockerClient()
  180. defer client.Close()
  181. if err != nil {
  182. log.Printf("Unable to create docker client: %s", err)
  183. return err
  184. }
  185. buf := new(bytes.Buffer)
  186. tw := tar.NewWriter(buf)
  187. defer tw.Close()
  188. log.Printf("[INFO] Setting up memory build structure for folder: %s", dockerfileFolder)
  189. err = getParsedTarMemory(fs, tw, dockerfileFolder, "")
  190. if err != nil {
  191. log.Printf("Tar issue: %s", err)
  192. return err
  193. }
  194. dockerFileTarReader := bytes.NewReader(buf.Bytes())
  195. // Dockerfile is inside the TAR itself. Not local context
  196. // docker build --build-arg http_proxy=http://my.proxy.url
  197. // Attempt at setting name according to #359: https://github.com/frikky/Shuffle/issues/359
  198. labels := map[string]string{}
  199. //target := ""
  200. //if len(tags) > 0 {
  201. // if strings.Contains(tags[0], ":") {
  202. // version := strings.Split(tags[0], ":")
  203. // if len(version) == 2 {
  204. // target = fmt.Sprintf("shuffle-build-%s", version[1])
  205. // tags = append(tags, target)
  206. // labels["name"] = target
  207. // }
  208. // }
  209. //}
  210. buildOptions := types.ImageBuildOptions{
  211. Remove: true,
  212. Tags: tags,
  213. BuildArgs: map[string]*string{},
  214. Labels: labels,
  215. }
  216. // NetworkMode: "host",
  217. httpProxy := os.Getenv("HTTP_PROXY")
  218. if len(httpProxy) > 0 {
  219. buildOptions.BuildArgs["HTTP_PROXY"] = &httpProxy
  220. }
  221. httpsProxy := os.Getenv("HTTPS_PROXY")
  222. if len(httpProxy) > 0 {
  223. buildOptions.BuildArgs["HTTPS_PROXY"] = &httpsProxy
  224. }
  225. // Build the actual image
  226. log.Printf(`[INFO] Building %s with proxy "%s". Tags: "%s". This may take up to a few minutes.`, dockerfileFolder, httpsProxy, strings.Join(tags, ","))
  227. imageBuildResponse, err := client.ImageBuild(
  228. ctx,
  229. dockerFileTarReader,
  230. buildOptions,
  231. )
  232. //log.Printf("RESPONSE: %#v", imageBuildResponse)
  233. //log.Printf("Response: %#v", imageBuildResponse.Body)
  234. //log.Printf("[DEBUG] IMAGERESPONSE: %#v", imageBuildResponse.Body)
  235. if imageBuildResponse.Body != nil {
  236. defer imageBuildResponse.Body.Close()
  237. buildBuf := new(strings.Builder)
  238. _, newerr := io.Copy(buildBuf, imageBuildResponse.Body)
  239. if newerr != nil {
  240. log.Printf("[WARNING] Failed reading Docker build STDOUT: %s", newerr)
  241. } else {
  242. log.Printf("[INFO] STRING: %s", buildBuf.String())
  243. if strings.Contains(buildBuf.String(), "errorDetail") {
  244. log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), strings.Join(tags, "\n"))
  245. // Handles pulling of the same image if applicable
  246. // This fixes some issues with older versions of Docker which can't build
  247. // on their own ( <17.05 )
  248. pullOptions := image.PullOptions{}
  249. downloaded := false
  250. for _, image := range tags {
  251. // Is this ok? Not sure. Tags shouldn't be controlled here prolly.
  252. image = strings.ToLower(image)
  253. newImage := fmt.Sprintf("%s/%s", registryName, image)
  254. log.Printf("[INFO] Pulling image %s", newImage)
  255. reader, err := client.ImagePull(ctx, newImage, pullOptions)
  256. if err != nil {
  257. log.Printf("[ERROR] Failed getting image %s: %s", newImage, err)
  258. continue
  259. }
  260. // Attempt to retag the image to not contain registry...
  261. //newBuf := buildBuf
  262. downloaded = true
  263. io.Copy(os.Stdout, reader)
  264. log.Printf("[INFO] Successfully downloaded and built %s", newImage)
  265. }
  266. if !downloaded {
  267. return errors.New(fmt.Sprintf("Failed to build / download images %s", strings.Join(tags, ",")))
  268. }
  269. //baseDockerName
  270. }
  271. }
  272. }
  273. if err != nil {
  274. // Read the STDOUT from the build process
  275. return err
  276. }
  277. return nil
  278. }
  279. func getK8sClient() (*kubernetes.Clientset, error) {
  280. config, err := rest.InClusterConfig()
  281. if err != nil {
  282. return nil, fmt.Errorf("[ERROR] failed to get in-cluster config: %v", err)
  283. }
  284. clientset, err := kubernetes.NewForConfig(config)
  285. if err != nil {
  286. return nil, fmt.Errorf("[ERROR] failed to create Kubernetes client: %v", err)
  287. }
  288. return clientset, nil
  289. }
  290. func deleteJob(client *kubernetes.Clientset, jobName, namespace string) error {
  291. deletePolicy := metav1.DeletePropagationForeground
  292. return client.BatchV1().Jobs(namespace).Delete(context.TODO(), jobName, metav1.DeleteOptions{
  293. PropagationPolicy: &deletePolicy,
  294. })
  295. }
  296. func buildImage(tags []string, dockerfileLocation string) error {
  297. isKubernetes := false
  298. if os.Getenv("IS_KUBERNETES") == "true" {
  299. isKubernetes = true
  300. }
  301. if isKubernetes {
  302. // log.Printf("K8S ###################")
  303. // log.Print("dockerfileFolder: ", dockerfileFolder)
  304. // log.Print("tags: ", tags)
  305. // log.Print("only tag: ", tags[1])
  306. registryName := ""
  307. if len(os.Getenv("REGISTRY_URL")) > 0 {
  308. registryName = os.Getenv("REGISTRY_URL")
  309. }
  310. log.Printf("[INFO] registry name: %s", registryName)
  311. contextDir := filepath.Join("/app/", filepath.Dir(dockerfileLocation))
  312. log.Print("contextDir: ", contextDir)
  313. client, err := getK8sClient()
  314. if err != nil {
  315. fmt.Printf("Unable to authencticate : %v\n", err)
  316. return err
  317. }
  318. BackendPodLabel := "io.kompose.service=backend"
  319. backendPodList, podListErr := client.CoreV1().Pods("shuffle").List(context.TODO(), metav1.ListOptions{
  320. LabelSelector: BackendPodLabel,
  321. })
  322. if podListErr != nil || len(backendPodList.Items) == 0 {
  323. fmt.Println("Error getting backend pod or no pod found:", podListErr)
  324. return podListErr
  325. }
  326. backendNodeName := backendPodList.Items[0].Spec.NodeName
  327. log.Printf("[INFO] Backend running on: %s", backendNodeName)
  328. job := &batchv1.Job{
  329. ObjectMeta: metav1.ObjectMeta{
  330. Name: "shuffle-app-builder",
  331. },
  332. Spec: batchv1.JobSpec{
  333. Template: corev1.PodTemplateSpec{
  334. Spec: corev1.PodSpec{
  335. Containers: []corev1.Container{
  336. {
  337. Name: "kaniko",
  338. Image: "gcr.io/kaniko-project/executor:latest",
  339. Args: []string{
  340. "--verbosity=debug",
  341. "--log-format=text",
  342. "--dockerfile=Dockerfile",
  343. "--context=dir://" + contextDir,
  344. "--skip-tls-verify",
  345. "--destination=" + registryName + "/" + tags[1],
  346. },
  347. VolumeMounts: []corev1.VolumeMount{
  348. {
  349. Name: "kaniko-workspace",
  350. MountPath: "/app/generated",
  351. },
  352. {
  353. Name: "docker-config",
  354. MountPath: "/kaniko/.docker/",
  355. },
  356. },
  357. },
  358. },
  359. NodeName: backendNodeName,
  360. RestartPolicy: corev1.RestartPolicyNever,
  361. Volumes: []corev1.Volume{
  362. {
  363. Name: "kaniko-workspace",
  364. VolumeSource: corev1.VolumeSource{
  365. PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
  366. ClaimName: "backend-apps-claim",
  367. },
  368. },
  369. },
  370. {
  371. Name: "docker-config",
  372. VolumeSource: corev1.VolumeSource{
  373. Secret: &corev1.SecretVolumeSource{
  374. SecretName: os.Getenv("SHUFFLE_REGISTRY_SECRET"),
  375. Items: []corev1.KeyToPath{
  376. {
  377. Key: ".dockerconfigjson",
  378. Path: "config.json",
  379. },
  380. },
  381. },
  382. },
  383. },
  384. },
  385. },
  386. },
  387. },
  388. }
  389. createdJob, err := client.BatchV1().Jobs("shuffle").Create(context.TODO(), job, metav1.CreateOptions{})
  390. if err != nil {
  391. log.Printf("Failed to start image builder job: %s", err)
  392. return err
  393. }
  394. timeout := time.After(5 * time.Minute)
  395. tick := time.Tick(5 * time.Second)
  396. for {
  397. select {
  398. case <-timeout:
  399. return fmt.Errorf("job didn't complete within the expected time")
  400. case <-tick:
  401. currentJob, err := client.BatchV1().Jobs("shuffle").Get(context.TODO(), createdJob.Name, metav1.GetOptions{})
  402. if err != nil {
  403. return fmt.Errorf("[ERROR] failed to fetch %s status: %v", createdJob.Name, err)
  404. }
  405. if currentJob.Status.Succeeded > 0 {
  406. log.Printf("[INFO] Job %s completed successfully!", createdJob.Name)
  407. log.Printf("[INFO] Cleaning up the job %s", createdJob.Name)
  408. err := deleteJob(client, createdJob.Name, "shuffle")
  409. if err != nil {
  410. return fmt.Errorf("[ERROR] failed deleting job %s with error: %s", createdJob.Name, err)
  411. }
  412. log.Println("Job deleted successfully!")
  413. return nil
  414. } else if currentJob.Status.Failed > 0 {
  415. log.Printf("[ERROR] %s job failed with error: %s", createdJob.Name, err)
  416. err := deleteJob(client, createdJob.Name, "shuffle")
  417. if err != nil {
  418. return fmt.Errorf("[ERROR] failed deleting job %s with error: %s", createdJob.Name, err)
  419. }
  420. }
  421. }
  422. }
  423. return nil
  424. }
  425. ctx := context.Background()
  426. // client, err := client.NewEnvClient()
  427. client, _, err := shuffle.GetDockerClient()
  428. defer client.Close()
  429. if err != nil {
  430. log.Printf("Unable to create docker client: %s", err)
  431. return err
  432. }
  433. log.Printf("[INFO] Docker Tags: %s", tags)
  434. log.Printf("[DEBUG] Dockerfile location: %s", dockerfileLocation)
  435. // Convert to forward slashes for consistent handling across OS
  436. normalizedPath := filepath.ToSlash(dockerfileLocation)
  437. dockerfileSplit := strings.Split(normalizedPath, "/")
  438. // Create a buffer
  439. buf := new(bytes.Buffer)
  440. tw := tar.NewWriter(buf)
  441. defer tw.Close()
  442. // Use the directory part of the dockerfile path
  443. baseDir := strings.Join(dockerfileSplit[0:len(dockerfileSplit)-1], "/")
  444. // Builds the entire folder into buf using OS-specific path
  445. err = getParsedTar(tw, filepath.FromSlash(baseDir), "")
  446. if err != nil {
  447. log.Printf("[ERROR] Tar issue during app build: %s", err)
  448. }
  449. dockerFileTarReader := bytes.NewReader(buf.Bytes())
  450. buildOptions := types.ImageBuildOptions{
  451. Remove: true,
  452. Tags: tags,
  453. BuildArgs: map[string]*string{},
  454. }
  455. //NetworkMode: "host",
  456. httpProxy := os.Getenv("HTTP_PROXY")
  457. if len(httpProxy) > 0 {
  458. buildOptions.BuildArgs["HTTP_PROXY"] = &httpProxy
  459. }
  460. httpsProxy := os.Getenv("HTTPS_PROXY")
  461. if len(httpProxy) > 0 {
  462. buildOptions.BuildArgs["https_proxy"] = &httpsProxy
  463. }
  464. // Print the actual file content from dockerFileTarReader
  465. /*
  466. data, err := ioutil.ReadAll(dockerFileTarReader)
  467. if err != nil {
  468. log.Printf("[ERROR] Failed reading Dockerfile TAR reader: %s", err)
  469. } else {
  470. log.Printf("[DEBUG] Dockerfile TAR reader content: %s", string(data))
  471. }
  472. */
  473. // Build the actual image
  474. imageBuildResponse, err := client.ImageBuild(
  475. ctx,
  476. dockerFileTarReader,
  477. buildOptions,
  478. )
  479. if err != nil {
  480. return err
  481. }
  482. // Read the STDOUT from the build process
  483. defer imageBuildResponse.Body.Close()
  484. buildBuf := new(strings.Builder)
  485. _, err = io.Copy(buildBuf, imageBuildResponse.Body)
  486. if err != nil {
  487. return err
  488. } else {
  489. if strings.Contains(buildBuf.String(), "errorDetail") {
  490. log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), strings.Join(tags, "\n"))
  491. return errors.New(fmt.Sprintf("Failed building %s. Check backend logs for details. Most likely means you have an old version of Docker.", strings.Join(tags, ",")))
  492. }
  493. }
  494. return nil
  495. }
  496. // Checks if an image exists
  497. func imageCheckBuilder(images []string) error {
  498. //log.Printf("[FIXME] ImageNames to check: %#v", images)
  499. return nil
  500. ctx := context.Background()
  501. // client, err := client.NewEnvClient()
  502. client, _, err := shuffle.GetDockerClient()
  503. if err != nil {
  504. log.Printf("Unable to create docker client: %s", err)
  505. return err
  506. }
  507. allImages, err := client.ImageList(ctx, image.ListOptions{
  508. All: true,
  509. })
  510. if err != nil {
  511. log.Printf("[ERROR] Failed creating imagelist: %s", err)
  512. return err
  513. }
  514. filteredImages := []image.Summary{}
  515. for _, image := range allImages {
  516. found := false
  517. for _, repoTag := range image.RepoTags {
  518. if strings.Contains(repoTag, baseDockerName) {
  519. found = true
  520. break
  521. }
  522. }
  523. if found {
  524. filteredImages = append(filteredImages, image)
  525. }
  526. }
  527. // FIXME: Continue fixing apps here
  528. // https://github.com/frikky/Shuffle/issues/135
  529. // 1. Find if app exists
  530. // 2. Create app if it doesn't
  531. //log.Printf("Apps: %#v", filteredImages)
  532. return nil
  533. }
  534. // https://stackoverflow.com/questions/23935141/how-to-copy-docker-images-from-one-host-to-another-without-using-a-repository
  535. func getDockerImage(resp http.ResponseWriter, request *http.Request) {
  536. cors := shuffle.HandleCors(resp, request)
  537. if cors {
  538. return
  539. }
  540. // Just here to verify that the user is logged in
  541. //_, err := shuffle.HandleApiAuthentication(resp, request)
  542. //if err != nil {
  543. // log.Printf("[WARNING] Api authentication failed in DOWNLOAD IMAGE: %s", err)
  544. // resp.WriteHeader(401)
  545. // resp.Write([]byte(`{"success": false}`))
  546. // return
  547. //}
  548. var err error
  549. body := []byte{}
  550. //log.Printf("IMAGE REQUEST BODY: %#v", request.Body)
  551. if request.Body == nil || request.Body == http.NoBody {
  552. // Check for the image query, otherwise we skip everything
  553. imageQuery := request.URL.Query().Get("image")
  554. if len(imageQuery) == 0 {
  555. resp.WriteHeader(400)
  556. resp.Write([]byte(`{"success": false, "reason": "No image query found"}`))
  557. return
  558. }
  559. body = []byte(fmt.Sprintf(`{"name": "%s"}`, imageQuery))
  560. } else {
  561. body, err = ioutil.ReadAll(request.Body)
  562. if err != nil {
  563. resp.WriteHeader(400)
  564. resp.Write([]byte(`{"success": false, "reason": "Failed reading body"}`))
  565. return
  566. }
  567. }
  568. // This has to be done in a weird way because Datastore doesn't
  569. // support map[string]interface and similar (openapi3.Swagger)
  570. var version shuffle.DockerRequestCheck
  571. err = json.Unmarshal(body, &version)
  572. if err != nil {
  573. resp.WriteHeader(422)
  574. resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed JSON marshalling: %s"}`, err)))
  575. return
  576. }
  577. //log.Printf("[DEBUG] Image to load: %s", version.Name)
  578. dockercli, _, err := shuffle.GetDockerClient()
  579. if err != nil {
  580. log.Printf("[WARNING] Unable to create docker client: %s", err)
  581. resp.WriteHeader(422)
  582. resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed JSON marshalling: %s"}`, err)))
  583. return
  584. }
  585. img := image.Summary{}
  586. img2 := image.Summary{}
  587. tagFound := ""
  588. tagFound2 := ""
  589. // Old way of doing it
  590. //alternativeNameSplit := strings.Split(version.Name, "/")
  591. //alternativeName := version.Name
  592. //if len(alternativeNameSplit) == 3 {
  593. // alternativeName = strings.Join(alternativeNameSplit[1:3], "/")
  594. //}
  595. appname, baseAppname, appnameSplit2, err := shuffle.GetAppNameSplit(version)
  596. if err != nil {
  597. log.Printf("[ERROR] Failed getting appname split: %s", err)
  598. resp.WriteHeader(500)
  599. resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "Couldn't get the right docker image name"}`)))
  600. return
  601. }
  602. if len(version.Name) == 0 {
  603. log.Printf("[ERROR] No image name provided for download: %s", version.Name)
  604. resp.WriteHeader(401)
  605. resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "No image name"}`)))
  606. return
  607. }
  608. log.Printf("[INFO] Trying to download image: '%s'. Appname: '%s'. BaseAppname: '%s', Split2: %s", version.Name, appname, baseAppname, appnameSplit2)
  609. alternativeName := appname
  610. ctx := context.Background()
  611. images, err := dockercli.ImageList(ctx, image.ListOptions{
  612. All: true,
  613. })
  614. for _, image := range images {
  615. for _, tag := range image.RepoTags {
  616. if strings.Contains(tag, "<none>") {
  617. continue
  618. }
  619. if strings.ToLower(tag) == strings.ToLower(version.Name) {
  620. img = image
  621. tagFound = tag
  622. break
  623. }
  624. if strings.ToLower(tag) == strings.ToLower(alternativeName) {
  625. img2 = image
  626. tagFound2 = tag
  627. }
  628. }
  629. }
  630. pullOptions := image.PullOptions{}
  631. if len(img.ID) == 0 {
  632. _, err := dockercli.ImagePull(context.Background(), version.Name, pullOptions)
  633. if err == nil {
  634. tagFound = version.Name
  635. img.ID = version.Name
  636. img2.ID = version.Name
  637. dockercli.ImageTag(ctx, version.Name, alternativeName)
  638. }
  639. }
  640. if len(img2.ID) == 0 {
  641. _, err := dockercli.ImagePull(context.Background(), alternativeName, pullOptions)
  642. if err == nil {
  643. tagFound = alternativeName
  644. img.ID = alternativeName
  645. img2.ID = alternativeName
  646. dockercli.ImageTag(ctx, alternativeName, version.Name)
  647. }
  648. }
  649. // REBUILDS THE APP
  650. if len(img.ID) == 0 {
  651. if len(img2.ID) == 0 {
  652. workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 0, 0)
  653. log.Printf("[INFO] Getting workflowapps for a rebuild. Got %d with err %#v", len(workflowapps), err)
  654. if err == nil {
  655. imageName := ""
  656. imageVersion := ""
  657. newNameSplit := strings.Split(version.Name, ":")
  658. if len(newNameSplit) == 2 {
  659. //log.Printf("[DEBUG] Found name %#v", newNameSplit)
  660. findVersionSplit := strings.Split(newNameSplit[1], "_")
  661. //log.Printf("[DEBUG] Found another split %#v", findVersionSplit)
  662. if len(findVersionSplit) == 2 {
  663. imageVersion = findVersionSplit[len(findVersionSplit)-1]
  664. imageName = findVersionSplit[0]
  665. } else if len(findVersionSplit) >= 2 {
  666. imageVersion = findVersionSplit[len(findVersionSplit)-1]
  667. imageName = strings.Join(findVersionSplit[0:len(findVersionSplit)-1], "_")
  668. } else {
  669. log.Printf("[DEBUG] Couldn't parse appname & version for %#v", findVersionSplit)
  670. }
  671. }
  672. if len(imageName) > 0 && len(imageVersion) > 0 {
  673. foundApp := shuffle.WorkflowApp{}
  674. imageName = strings.ToLower(imageName)
  675. imageVersion = strings.ToLower(imageVersion)
  676. log.Printf("[DEBUG] Docker Looking for appname %s with version %s", imageName, imageVersion)
  677. for _, app := range workflowapps {
  678. if strings.ToLower(strings.Replace(app.Name, " ", "_", -1)) == imageName && app.AppVersion == imageVersion {
  679. if app.Generated {
  680. log.Printf("[DEBUG] Found matching app %s:%s - %s", imageName, imageVersion, app.ID)
  681. foundApp = app
  682. break
  683. } else {
  684. log.Printf("[WARNING] Trying to rebuild app that isn't generated - not allowed. Looking further.")
  685. }
  686. //break
  687. }
  688. }
  689. if len(foundApp.ID) > 0 {
  690. openApiApp, err := shuffle.GetOpenApiDatastore(ctx, foundApp.ID)
  691. if err != nil {
  692. log.Printf("[ERROR] Failed getting OpenAPI app %s to database: %s", foundApp.ID, err)
  693. } else {
  694. log.Printf("[DEBUG] Found OpenAPI app for %s as generated - now building!", version.Name)
  695. user := shuffle.User{}
  696. //img = version.Name
  697. if len(alternativeName) > 0 {
  698. tagFound = alternativeName
  699. } else {
  700. tagFound = version.Name
  701. }
  702. buildSwaggerApp(resp, []byte(openApiApp.Body), user, false)
  703. }
  704. }
  705. }
  706. } else {
  707. log.Printf("[WARNING] Couldn't find an image with registry name %s and %s", version.Name, alternativeName)
  708. resp.WriteHeader(401)
  709. resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "Couldn't find image %s"}`, version.Name)))
  710. return
  711. }
  712. }
  713. if len(tagFound) == 0 && len(tagFound2) > 0 {
  714. img = img2
  715. tagFound = tagFound2
  716. }
  717. }
  718. //log.Printf("[INFO] Img found (%s): %#v", tagFound, img)
  719. //log.Printf("[INFO] Img found to be downloaded by client: %s", tagFound)
  720. newClient, err := newdockerclient.NewClientFromEnv()
  721. if err != nil {
  722. log.Printf("[ERROR] Failed setting up docker env: %#v", newClient)
  723. resp.WriteHeader(401)
  724. resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "Couldn't make docker client"}`)))
  725. return
  726. }
  727. ////https://github.com/fsouza/go-dockerclient/issues/600
  728. //defer fileReader.Close()
  729. opts := newdockerclient.ExportImageOptions{
  730. Name: tagFound,
  731. OutputStream: resp,
  732. }
  733. if err := newClient.ExportImage(opts); err != nil {
  734. log.Printf("[ERROR] FAILED to save image to file: %s", err)
  735. resp.WriteHeader(401)
  736. resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "Couldn't export image"}`)))
  737. return
  738. }
  739. //resp.WriteHeader(200)
  740. }
  741. // Downloads and activates an app from shuffler.io if possible
  742. func handleRemoteDownloadApp(resp http.ResponseWriter, ctx context.Context, user shuffle.User, appId string) {
  743. url := fmt.Sprintf("https://shuffler.io/api/v1/apps/%s/config", appId)
  744. log.Printf("[DEBUG] Downloading API from URL %s", url)
  745. req, err := http.NewRequest(
  746. "GET",
  747. url,
  748. nil,
  749. )
  750. if err != nil {
  751. log.Printf("[ERROR] Failed auto-downloading app %s: %s", appId, err)
  752. resp.WriteHeader(401)
  753. resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
  754. return
  755. }
  756. httpClient := shuffle.GetExternalClient(url)
  757. newresp, err := httpClient.Do(req)
  758. if err != nil {
  759. log.Printf("[ERROR] Failed running auto-download request for %s: %s", appId, err)
  760. resp.WriteHeader(401)
  761. resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
  762. return
  763. }
  764. defer newresp.Body.Close()
  765. respBody, err := ioutil.ReadAll(newresp.Body)
  766. if err != nil {
  767. log.Printf("[ERROR] Failed setting respbody for workflow download: %s", err)
  768. resp.WriteHeader(401)
  769. resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
  770. return
  771. }
  772. if len(respBody) > 0 {
  773. type tmpapp struct {
  774. Success bool `json:"success"`
  775. OpenAPI string `json:"openapi"`
  776. App string `json:"app"`
  777. }
  778. app := tmpapp{}
  779. err := json.Unmarshal(respBody, &app)
  780. if err != nil || app.Success == false || len(app.OpenAPI) == 0 {
  781. log.Printf("[ERROR] Failed app unmarshal during auto-download. Success: %#v. Applength: %d: %s", app.Success, len(app.OpenAPI), err)
  782. resp.WriteHeader(401)
  783. if len(app.App) > 0 {
  784. resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Not an OpenAPI app, but a Python app. Please download the app using the Remote Download system: https://shuffler.io/docs/apps#importing-remote-apps"}`)))
  785. } else {
  786. resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
  787. }
  788. resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
  789. return
  790. }
  791. key, err := base64.StdEncoding.DecodeString(app.OpenAPI)
  792. if err != nil {
  793. log.Printf("[ERROR] Failed auto-setting OpenAPI app: %s", err)
  794. resp.WriteHeader(401)
  795. resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
  796. return
  797. }
  798. cacheKey := fmt.Sprintf("workflowapps-sorted-100")
  799. shuffle.DeleteCache(ctx, cacheKey)
  800. cacheKey = fmt.Sprintf("workflowapps-sorted-500")
  801. shuffle.DeleteCache(ctx, cacheKey)
  802. cacheKey = fmt.Sprintf("workflowapps-sorted-1000")
  803. shuffle.DeleteCache(ctx, cacheKey)
  804. newapp := shuffle.ParsedOpenApi{}
  805. err = json.Unmarshal(key, &newapp)
  806. if err != nil {
  807. log.Printf("[ERROR] Failed openapi unmarshal during auto-download: %s", app.Success, len(app.OpenAPI), err)
  808. resp.WriteHeader(401)
  809. resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
  810. return
  811. }
  812. err = json.Unmarshal(key, &newapp)
  813. if err != nil {
  814. log.Printf("[ERROR] Failed openapi unmarshal during auto-download: %s", app.Success, len(app.OpenAPI), err)
  815. resp.WriteHeader(401)
  816. resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
  817. return
  818. }
  819. buildSwaggerApp(resp, []byte(newapp.Body), user, true)
  820. return
  821. }
  822. }
  823. func activateWorkflowAppDocker(resp http.ResponseWriter, request *http.Request) {
  824. cors := shuffle.HandleCors(resp, request)
  825. if cors {
  826. return
  827. }
  828. user, err := shuffle.HandleApiAuthentication(resp, request)
  829. if err != nil {
  830. log.Printf("[WARNING] Api authentication failed in get active apps: %s", err)
  831. resp.WriteHeader(401)
  832. resp.Write([]byte(`{"success": false}`))
  833. return
  834. }
  835. if user.Role == "org-reader" {
  836. log.Printf("[WARNING] Org-reader doesn't have access to activate workflow app (shared): %s (%s)", user.Username, user.Id)
  837. resp.WriteHeader(401)
  838. resp.Write([]byte(`{"success": false, "reason": "Read only user"}`))
  839. return
  840. }
  841. ctx := context.Background()
  842. location := strings.Split(request.URL.String(), "/")
  843. var fileId string
  844. if location[1] == "api" {
  845. if len(location) <= 4 {
  846. resp.WriteHeader(401)
  847. resp.Write([]byte(`{"success": false}`))
  848. return
  849. }
  850. fileId = location[4]
  851. }
  852. app, err := shuffle.GetApp(ctx, fileId, user, false)
  853. if err != nil {
  854. appName := request.URL.Query().Get("app_name")
  855. appVersion := request.URL.Query().Get("app_version")
  856. if len(appName) > 0 && len(appVersion) > 0 {
  857. apps, err := shuffle.FindWorkflowAppByName(ctx, appName)
  858. //log.Printf("[INFO] Found %d apps for %s", len(apps), appName)
  859. if err != nil || len(apps) == 0 {
  860. log.Printf("[WARNING] Error getting app %s (app config). Starting remote download.: %s", appName, err)
  861. handleRemoteDownloadApp(resp, ctx, user, fileId)
  862. return
  863. }
  864. selectedApp := shuffle.WorkflowApp{}
  865. for _, app := range apps {
  866. if !app.Sharing && !app.Public {
  867. continue
  868. }
  869. if app.Name == appName {
  870. selectedApp = app
  871. }
  872. if app.Name == appName && app.AppVersion == appVersion {
  873. selectedApp = app
  874. }
  875. }
  876. app = &selectedApp
  877. } else {
  878. log.Printf("[WARNING] Error getting app with ID %s (app config): %s. Starting remote download(2)", fileId, err)
  879. handleRemoteDownloadApp(resp, ctx, user, fileId)
  880. return
  881. //resp.WriteHeader(401)
  882. //resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
  883. //return
  884. }
  885. }
  886. // Just making sure it's being built properly
  887. if app == nil {
  888. log.Printf("[WARNING] App is nil. This shouldn't happen. Starting remote download(3)")
  889. handleRemoteDownloadApp(resp, ctx, user, fileId)
  890. return
  891. }
  892. // Check the app.. hmm
  893. openApiApp, err := shuffle.GetOpenApiDatastore(ctx, app.ID)
  894. if err != nil {
  895. log.Printf("[WARNING] Error getting app %s (openapi config): %s", app.ID, err)
  896. resp.WriteHeader(401)
  897. resp.Write([]byte(`{"success": false, "reason": "Couldn't find app OpenAPI"}`))
  898. return
  899. }
  900. log.Printf("[INFO] User %s (%s) is activating %s. Public: %t, Shared: %t", user.Username, user.Id, app.Name, app.Public, app.Sharing)
  901. buildSwaggerApp(resp, []byte(openApiApp.Body), user, true)
  902. }