Merge pull request #2426 from alecmerdler/frontend-refactoring
More Front-end TypeScript Refactoring
This commit is contained in:
		
						commit
						cd61482ff1
					
				
					 26 changed files with 1250 additions and 441 deletions
				
			
		|  | @ -10,10 +10,8 @@ angular.module('quay').directive('dockerfileBuildForm', function () { | |||
|     restrict: 'C', | ||||
|     scope: { | ||||
|       'repository': '=repository', | ||||
| 
 | ||||
|       'isReady': '=?isReady', | ||||
|       'reset': '=?reset', | ||||
| 
 | ||||
|       'readyForBuild': '&readyForBuild' | ||||
|     }, | ||||
|     controller: function($scope, $element, ApiService, DockerfileService, Config) { | ||||
|  | @ -39,25 +37,27 @@ angular.module('quay').directive('dockerfileBuildForm', function () { | |||
|         $scope.state = 'checking'; | ||||
|         $scope.selectedFiles = files; | ||||
| 
 | ||||
|         DockerfileService.getDockerfile(files[0], function(df) { | ||||
|           var baseImage = df.getRegistryBaseImage(); | ||||
|           if (baseImage) { | ||||
|             checkPrivateImage(baseImage); | ||||
|           } else { | ||||
|             $scope.state = 'ready'; | ||||
|           } | ||||
|         DockerfileService.getDockerfile(files[0]) | ||||
|           .then(function(dockerfileInfo) { | ||||
|             var baseImage = dockerfileInfo.getRegistryBaseImage(); | ||||
|             if (baseImage) { | ||||
|               checkPrivateImage(baseImage); | ||||
|             } else { | ||||
|               $scope.state = 'ready'; | ||||
|             } | ||||
| 
 | ||||
|           $scope.$apply(function() { | ||||
|             opt_callback && opt_callback(true, 'Dockerfile found and valid') | ||||
|           }); | ||||
|         }, function(msg) { | ||||
|           $scope.state = 'empty'; | ||||
|           $scope.privateBaseRepository = null; | ||||
|             $scope.$apply(function() { | ||||
|               opt_callback && opt_callback(true, 'Dockerfile found and valid') | ||||
|             }); | ||||
|           }) | ||||
|           .catch(function(error) { | ||||
|             $scope.state = 'empty'; | ||||
|             $scope.privateBaseRepository = null; | ||||
| 
 | ||||
|           $scope.$apply(function() { | ||||
|             opt_callback && opt_callback(false, msg || 'Could not find valid Dockerfile'); | ||||
|             $scope.$apply(function() { | ||||
|               opt_callback && opt_callback(false, error || 'Could not find valid Dockerfile'); | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.handleFilesCleared = function() { | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| import * as angular from "angular"; | ||||
| import 'core-js'; | ||||
| import { ViewArrayImpl } from "./services/view-array/view-array.impl"; | ||||
| import { NAME_PATTERNS } from "./constants/name-patterns.constant"; | ||||
| import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant"; | ||||
|  | @ -12,6 +13,10 @@ import { LinearWorkflowComponent } from './directives/ui/linear-workflow/linear- | |||
| import { LinearWorkflowSectionComponent } from './directives/ui/linear-workflow/linear-workflow-section.component'; | ||||
| import { QuayConfig } from './quay-config.module'; | ||||
| import { QuayRun } from './quay-run.module'; | ||||
| import { BuildServiceImpl } from './services/build/build.service.impl'; | ||||
| import { AvatarServiceImpl } from './services/avatar/avatar.service.impl'; | ||||
| import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.impl'; | ||||
| import { DataFileServiceImpl } from './services/datafile/datafile.service.impl'; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  | @ -33,6 +38,10 @@ import { QuayRun } from './quay-run.module'; | |||
|   ], | ||||
|   providers: [ | ||||
|     ViewArrayImpl, | ||||
|     BuildServiceImpl, | ||||
|     AvatarServiceImpl, | ||||
|     DockerfileServiceImpl, | ||||
|     DataFileServiceImpl, | ||||
|   ], | ||||
| }) | ||||
| export class quay { | ||||
|  | @ -42,6 +51,7 @@ export class quay { | |||
| // TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2
 | ||||
| angular | ||||
|   .module(quay.name) | ||||
|   .factory("fileReaderFactory", () => () => new FileReader()) | ||||
|   .constant('NAME_PATTERNS', NAME_PATTERNS) | ||||
|   .constant('INJECTED_CONFIG', INJECTED_CONFIG) | ||||
|   .constant('INJECTED_FEATURES', INJECTED_FEATURES) | ||||
|  |  | |||
|  | @ -1,49 +0,0 @@ | |||
| /** | ||||
|  * Service which provides helper methods for retrieving the avatars displayed in the app. | ||||
|  */ | ||||
| angular.module('quay').factory('AvatarService', ['Config', '$sanitize', 'md5', | ||||
|     function(Config, $sanitize, md5) { | ||||
|   var avatarService = {}; | ||||
|   var cache = {}; | ||||
| 
 | ||||
|   avatarService.getAvatar = function(hash, opt_size, opt_notfound) { | ||||
|     var size = opt_size || 16; | ||||
|     switch (Config['AVATAR_KIND']) { | ||||
|       case 'local': | ||||
|         return '/avatar/' + hash + '?size=' + size; | ||||
|         break; | ||||
| 
 | ||||
|       case 'gravatar': | ||||
|         var notfound = opt_notfound || '404'; | ||||
|         return '//www.gravatar.com/avatar/' + hash + '?d=' + notfound + '&size=' + size; | ||||
|         break; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   avatarService.computeHash = function(opt_email, opt_name) { | ||||
|     var email = opt_email || ''; | ||||
|     var name = opt_name || ''; | ||||
| 
 | ||||
|     var cacheKey = email + ':' + name; | ||||
|     if (!cacheKey) { return '-'; } | ||||
| 
 | ||||
|     if (cache[cacheKey]) { | ||||
|       return cache[cacheKey]; | ||||
|     } | ||||
| 
 | ||||
|     var hash = md5.createHash(email.toString().toLowerCase()); | ||||
|     switch (Config['AVATAR_KIND']) { | ||||
|       case 'local': | ||||
|         if (name) { | ||||
|           hash = name[0] + hash; | ||||
|         } else if (email) { | ||||
|           hash = email[0] + hash; | ||||
|         } | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     return cache[cacheKey] = hash; | ||||
|   }; | ||||
| 
 | ||||
|   return avatarService; | ||||
| }]); | ||||
							
								
								
									
										97
									
								
								static/js/services/avatar/avatar.service.impl.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								static/js/services/avatar/avatar.service.impl.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,97 @@ | |||
| import { AvatarServiceImpl } from './avatar.service.impl'; | ||||
| 
 | ||||
| 
 | ||||
| describe("AvatarServiceImpl", () => { | ||||
|   var avatarServiceImpl: AvatarServiceImpl; | ||||
|   var configMock: any; | ||||
|   var md5Mock: any; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     configMock = {AVATAR_KIND: 'local'}; | ||||
|     md5Mock = jasmine.createSpyObj('md5Mock', ['createHash']); | ||||
|     avatarServiceImpl = new AvatarServiceImpl(configMock, md5Mock); | ||||
|   }); | ||||
| 
 | ||||
|   describe("getAvatar", () => { | ||||
|     var hash: string; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       hash = "a1b2c3d4e5f6"; | ||||
|     }); | ||||
| 
 | ||||
|     it("returns a local avatar URL if given config has avatar kind set to local", () => { | ||||
|       var avatarURL: string = avatarServiceImpl.getAvatar(hash); | ||||
| 
 | ||||
|       expect(avatarURL).toEqual(`/avatar/${hash}?size=16`); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns a Gravatar URL if given config has avatar kind set to Gravatar", () => { | ||||
|       configMock['AVATAR_KIND'] = 'gravatar'; | ||||
|       var avatarURL: string = avatarServiceImpl.getAvatar(hash); | ||||
| 
 | ||||
|       expect(avatarURL).toEqual(`//www.gravatar.com/avatar/${hash}?d=404&size=16`); | ||||
|     }); | ||||
| 
 | ||||
|     it("uses 16 as default size query parameter if not provided", () => { | ||||
|       var size: number = 16; | ||||
|       var avatarURL: string = avatarServiceImpl.getAvatar(hash); | ||||
| 
 | ||||
|       expect(avatarURL).toEqual(`/avatar/${hash}?size=${size}`); | ||||
|     }); | ||||
| 
 | ||||
|     it("uses 404 as default not found query parameter for Gravatar URL if not provided", () => { | ||||
|       configMock['AVATAR_KIND'] = 'gravatar'; | ||||
|       var notFound: string = '404'; | ||||
|       var avatarURL: string = avatarServiceImpl.getAvatar(hash); | ||||
| 
 | ||||
|       expect(avatarURL).toEqual(`//www.gravatar.com/avatar/${hash}?d=${notFound}&size=16`); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe("computeHash", () => { | ||||
|     var email: string; | ||||
|     var name: string; | ||||
|     var expectedHash: string; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       email = "some_example@gmail.com"; | ||||
|       name = "example"; | ||||
|       expectedHash = "a1b2c3d4e5f6"; | ||||
|       md5Mock.createHash = jasmine.createSpy('createHashSpy').and.returnValue(expectedHash); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns hash from cache if it exists", () => { | ||||
|       // Call once to set the cache
 | ||||
|       avatarServiceImpl.computeHash(email, name); | ||||
|       md5Mock.createHash.calls.reset(); | ||||
|       avatarServiceImpl.computeHash(email, name); | ||||
| 
 | ||||
|       expect(md5Mock.createHash).not.toHaveBeenCalled(); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls MD5 service to create hash using given email if cache is not set", () => { | ||||
|       avatarServiceImpl.computeHash(email, name); | ||||
| 
 | ||||
|       expect(md5Mock.createHash.calls.argsFor(0)[0]).toEqual(email.toString().toLowerCase()); | ||||
|     }); | ||||
| 
 | ||||
|     it("adds first character of given name to hash if config has avatar kind set to local", () => { | ||||
|       var hash: string = avatarServiceImpl.computeHash(email, name); | ||||
| 
 | ||||
|       expect(hash[0]).toEqual(name[0]); | ||||
|     }); | ||||
| 
 | ||||
|     it("adds first character of given email to hash if config has avatar kind set to local and not given name", () => { | ||||
|       var hash: string = avatarServiceImpl.computeHash(email); | ||||
| 
 | ||||
|       expect(hash[0]).toEqual(email[0]); | ||||
|     }); | ||||
| 
 | ||||
|     it("adds nothing to hash if config avatar kind is not set to local", () => { | ||||
|       configMock['AVATAR_KIND'] = 'gravatar'; | ||||
|       var hash: string = avatarServiceImpl.computeHash(email); | ||||
| 
 | ||||
|       expect(hash).toEqual(expectedHash); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										49
									
								
								static/js/services/avatar/avatar.service.impl.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								static/js/services/avatar/avatar.service.impl.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | |||
| import { AvatarService } from './avatar.service'; | ||||
| import { Injectable } from 'angular-ts-decorators'; | ||||
| 
 | ||||
| 
 | ||||
| @Injectable(AvatarService.name) | ||||
| export class AvatarServiceImpl implements AvatarService { | ||||
| 
 | ||||
|   private cache: {[cacheKey: string]: string} = {}; | ||||
| 
 | ||||
|   constructor(private Config: any, private md5: any) { | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   public getAvatar(hash: string, size: number = 16, notFound: string = '404'): string { | ||||
|     var avatarURL: string; | ||||
|     switch (this.Config['AVATAR_KIND']) { | ||||
|       case 'local': | ||||
|         avatarURL = `/avatar/${hash}?size=${size}`; | ||||
|         break; | ||||
| 
 | ||||
|       case 'gravatar': | ||||
|         avatarURL = `//www.gravatar.com/avatar/${hash}?d=${notFound}&size=${size}`; | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     return avatarURL; | ||||
|   } | ||||
| 
 | ||||
|   public computeHash(email: string = '', name: string = ''): string { | ||||
|     const cacheKey: string = email + ':' + name; | ||||
| 
 | ||||
|     if (this.cache[cacheKey]) { | ||||
|       return this.cache[cacheKey]; | ||||
|     } | ||||
| 
 | ||||
|     var hash: string = this.md5.createHash(email.toString().toLowerCase()); | ||||
|     switch (this.Config['AVATAR_KIND']) { | ||||
|       case 'local': | ||||
|         if (name) { | ||||
|           hash = name[0] + hash; | ||||
|         } else if (email) { | ||||
|           hash = email[0] + hash; | ||||
|         } | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     return this.cache[cacheKey] = hash; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										22
									
								
								static/js/services/avatar/avatar.service.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								static/js/services/avatar/avatar.service.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| /** | ||||
|  * Service which provides helper methods for retrieving the avatars displayed in the app. | ||||
|  */ | ||||
| export abstract class AvatarService { | ||||
| 
 | ||||
|   /** | ||||
|    * Retrieve URL for avatar image with given hash. | ||||
|    * @param hash Avatar image hash. | ||||
|    * @param size Avatar image size. | ||||
|    * @param notFound URL parameter if avatar image is not found. | ||||
|    * @return avatarURL The URL for the avatar image. | ||||
|    */ | ||||
|   public abstract getAvatar(hash: string, size?: number, notFound?: string): string; | ||||
| 
 | ||||
|   /** | ||||
|    * Compute the avatar image hash. | ||||
|    * @param email Email for avatar user. | ||||
|    * @param name Username for avatar user. | ||||
|    * @return hash The hash for the avatar image. | ||||
|    */ | ||||
|   public abstract computeHash(email?: string, name?: string): string; | ||||
| } | ||||
|  | @ -1,62 +0,0 @@ | |||
| /** | ||||
|  * Service which provides helper methods for reasoning about builds. | ||||
|  */ | ||||
| angular.module('quay').factory('BuildService', [function() { | ||||
|   var buildService = {}; | ||||
|   buildService.isActive = function(build) { | ||||
|     return build.phase != 'complete' && build.phase != 'error' && build.phase != 'expired' && build.phase != 'cancelled'; | ||||
|   }; | ||||
| 
 | ||||
|   buildService.getBuildMessage = function(phase) { | ||||
|     switch (phase) { | ||||
|       case 'cannot_load': | ||||
|         return 'Cannot load build status'; | ||||
| 
 | ||||
|       case 'starting': | ||||
|       case 'initializing': | ||||
|         return 'Starting Dockerfile build'; | ||||
| 
 | ||||
|       case 'waiting': | ||||
|         return 'Waiting for available build worker'; | ||||
| 
 | ||||
|       case 'unpacking': | ||||
|         return 'Unpacking build package'; | ||||
| 
 | ||||
|       case 'pulling': | ||||
|         return 'Pulling base image'; | ||||
| 
 | ||||
|       case 'building': | ||||
|         return 'Building image from Dockerfile'; | ||||
| 
 | ||||
|       case 'checking-cache': | ||||
|         return 'Looking up cached images'; | ||||
| 
 | ||||
|       case 'priming-cache': | ||||
|         return 'Priming cache for build'; | ||||
| 
 | ||||
|       case 'build-scheduled': | ||||
|         return 'Preparing build node'; | ||||
| 
 | ||||
|       case 'pushing': | ||||
|         return 'Pushing image built from Dockerfile'; | ||||
| 
 | ||||
|       case 'complete': | ||||
|         return 'Dockerfile build completed and pushed'; | ||||
| 
 | ||||
|       case 'error': | ||||
|         return 'Dockerfile build failed'; | ||||
| 
 | ||||
|       case 'expired': | ||||
|         return 'Build did not complete after 3 attempts. Re-submit this build to try again.'; | ||||
| 
 | ||||
|       case 'internalerror': | ||||
|         return 'An internal system error occurred while building; the build will be retried in the next few minutes.'; | ||||
| 
 | ||||
|       case 'cancelled': | ||||
|         return 'This build was previously cancelled.'; | ||||
| 
 | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return buildService; | ||||
| }]); | ||||
							
								
								
									
										76
									
								
								static/js/services/build/build.service.impl.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								static/js/services/build/build.service.impl.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,76 @@ | |||
| import { BuildServiceImpl } from './build.service.impl'; | ||||
| 
 | ||||
| 
 | ||||
| describe("BuildServiceImpl", () => { | ||||
|   var buildServiceImpl: BuildServiceImpl; | ||||
|   var build: {phase: string}; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     buildServiceImpl = new BuildServiceImpl(); | ||||
|     build = {phase: ""}; | ||||
|   }); | ||||
| 
 | ||||
|   describe("isActive", () => { | ||||
|     var phases: string[]; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       phases = ['complete', 'error', 'expired', 'cancelled']; | ||||
|     }); | ||||
| 
 | ||||
|     it("returns false if given build's phase matches an inactive phase", () => { | ||||
|       phases.forEach((phase: string) => { | ||||
|         build.phase = phase; | ||||
| 
 | ||||
|         expect(buildServiceImpl.isActive(build)).toBe(false); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns true if given build's phase does not match inactive phases", () => { | ||||
|       build.phase = 'initializing'; | ||||
| 
 | ||||
|       expect(buildServiceImpl.isActive(build)).toBe(true); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe("getBuildMessage", () => { | ||||
|     var buildMessages: {phase: string, message: string}[]; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       buildMessages = [ | ||||
|         {phase: 'cannot_load', message: 'Cannot load build status'}, | ||||
|         {phase: 'starting', message: 'Starting Dockerfile build'}, | ||||
|         {phase: 'initializing', message: 'Starting Dockerfile build'}, | ||||
|         {phase: 'waiting', message: 'Waiting for available build worker'}, | ||||
|         {phase: 'unpacking', message: 'Unpacking build package'}, | ||||
|         {phase: 'pulling', message: 'Pulling base image'}, | ||||
|         {phase: 'building', message: 'Building image from Dockerfile'}, | ||||
|         {phase: 'checking-cache', message: 'Looking up cached images'}, | ||||
|         {phase: 'priming-cache', message: 'Priming cache for build'}, | ||||
|         {phase: 'build-scheduled', message: 'Preparing build node'}, | ||||
|         {phase: 'pushing', message: 'Pushing image built from Dockerfile'}, | ||||
|         {phase: 'complete', message: 'Dockerfile build completed and pushed'}, | ||||
|         {phase: 'error', message: 'Dockerfile build failed'}, | ||||
|         {phase: 'expired', message: 'Build did not complete after 3 attempts. Re-submit this build to try again.'}, | ||||
|         {phase: 'internalerror', message: 'An internal system error occurred while building; the build will be retried in the next few minutes.'}, | ||||
|         {phase: 'cancelled', message: 'This build was previously cancelled.'}, | ||||
|       ]; | ||||
|     }); | ||||
| 
 | ||||
|     it("returns the correct message for the given phase", () => { | ||||
|       buildMessages.forEach((buildMessage) => { | ||||
|         expect(buildServiceImpl.getBuildMessage(buildMessage.phase)).toEqual(buildMessage.message, buildMessage); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("throws an error if given phase is not supported", () => { | ||||
|       var phase: string = "not-a-phase"; | ||||
| 
 | ||||
|       try { | ||||
|         buildServiceImpl.getBuildMessage(phase); | ||||
|         fail("Should throw error"); | ||||
|       } catch (error) { | ||||
|         expect(error.message).toEqual("Invalid build phase"); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										84
									
								
								static/js/services/build/build.service.impl.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								static/js/services/build/build.service.impl.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | |||
| import { BuildService } from './build.service'; | ||||
| import { Injectable } from 'angular-ts-decorators'; | ||||
| 
 | ||||
| 
 | ||||
| @Injectable(BuildService.name) | ||||
| export class BuildServiceImpl implements BuildService { | ||||
| 
 | ||||
|   private inactivePhases: string[] = ['complete', 'error', 'expired', 'cancelled']; | ||||
| 
 | ||||
|   public isActive(build: {phase: string}): boolean { | ||||
|     return this.inactivePhases.indexOf(build.phase) == -1; | ||||
|   } | ||||
| 
 | ||||
|   public getBuildMessage(phase: string): string { | ||||
|     var message: string; | ||||
|     switch (phase) { | ||||
|       case 'cannot_load': | ||||
|         message = 'Cannot load build status'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'starting': | ||||
|       case 'initializing': | ||||
|         message = 'Starting Dockerfile build'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'waiting': | ||||
|         message = 'Waiting for available build worker'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'unpacking': | ||||
|         message = 'Unpacking build package'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'pulling': | ||||
|         message = 'Pulling base image'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'building': | ||||
|         message = 'Building image from Dockerfile'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'checking-cache': | ||||
|         message = 'Looking up cached images'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'priming-cache': | ||||
|         message = 'Priming cache for build'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'build-scheduled': | ||||
|         message = 'Preparing build node'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'pushing': | ||||
|         message = 'Pushing image built from Dockerfile'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'complete': | ||||
|         message = 'Dockerfile build completed and pushed'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'error': | ||||
|         message = 'Dockerfile build failed'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'expired': | ||||
|         message = 'Build did not complete after 3 attempts. Re-submit this build to try again.'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'internalerror': | ||||
|         message = 'An internal system error occurred while building; the build will be retried in the next few minutes.'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'cancelled': | ||||
|         message = 'This build was previously cancelled.'; | ||||
|         break; | ||||
| 
 | ||||
|       default: | ||||
|         throw new Error("Invalid build phase"); | ||||
|     } | ||||
| 
 | ||||
|     return message; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										19
									
								
								static/js/services/build/build.service.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								static/js/services/build/build.service.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| /** | ||||
|  * Service which provides helper methods for reasoning about builds. | ||||
|  */ | ||||
| export abstract class BuildService { | ||||
| 
 | ||||
|   /** | ||||
|    * Determine if the given build is active. | ||||
|    * @param build The build object. | ||||
|    * @return isActive If the given build is active. | ||||
|    */ | ||||
|   public abstract isActive(build: {phase: string}): boolean; | ||||
| 
 | ||||
|   /** | ||||
|    * Generate a message based on a given phase. | ||||
|    * @param phase The phase type. | ||||
|    * @return buildMessage The message associated with the given phase. | ||||
|    */ | ||||
|   public abstract getBuildMessage(phase: string): string; | ||||
| } | ||||
|  | @ -1,176 +0,0 @@ | |||
| /** | ||||
|  * Service which provides helper methods for downloading a data file from a URL, and extracting | ||||
|  * its contents as .tar, .tar.gz, or .zip file. Note that this service depends on external | ||||
|  * library code in the lib/ directory: | ||||
|  *  - jszip.min.js | ||||
|  *  - Blob.js | ||||
|  *  - zlib.js | ||||
|  */ | ||||
| angular.module('quay').factory('DataFileService', [function() { | ||||
|   var dataFileService = {}; | ||||
| 
 | ||||
|   dataFileService.getName_ = function(filePath) { | ||||
|     var parts = filePath.split('/'); | ||||
|     return parts[parts.length - 1]; | ||||
|   }; | ||||
| 
 | ||||
|   dataFileService.tryAsZip_ = function(buf, success, failure) { | ||||
|     var zip = null; | ||||
|     var zipFiles = null; | ||||
|     try { | ||||
|       var zip = new JSZip(buf); | ||||
|       zipFiles = zip.files; | ||||
|     } catch (e) { | ||||
|       failure(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     var files = []; | ||||
|     for (var filePath in zipFiles) { | ||||
|       if (zipFiles.hasOwnProperty(filePath)) { | ||||
|         files.push({ | ||||
|           'name': dataFileService.getName_(filePath), | ||||
|           'path': filePath, | ||||
|           'canRead': true, | ||||
|           'toBlob': (function(fp) { | ||||
|             return function() { | ||||
|               return new Blob([zip.file(fp).asArrayBuffer()]); | ||||
|             }; | ||||
|           }(filePath)) | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     success(files); | ||||
|   }; | ||||
| 
 | ||||
|   dataFileService.tryAsTarGz_ = function(buf, success, failure) { | ||||
|     var gunzip = new Zlib.Gunzip(new Uint8Array(buf)); | ||||
|     var plain = null; | ||||
| 
 | ||||
|     try { | ||||
|       plain = gunzip.decompress(); | ||||
|     } catch (e) { | ||||
|       failure(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (plain.byteLength == 0) { | ||||
|       plain = buf; | ||||
|     } | ||||
| 
 | ||||
|     dataFileService.tryAsTar_(plain, success, failure); | ||||
|   }; | ||||
| 
 | ||||
|   dataFileService.tryAsTar_ = function(buf, success, failure) { | ||||
|     var collapsePath = function(originalPath) { | ||||
|       // Tar files can contain entries of the form './', so we need to collapse
 | ||||
|       // those paths down.
 | ||||
|       var parts = originalPath.split('/'); | ||||
|       for (var i = parts.length - 1; i >= 0; i--) { | ||||
|         var part = parts[i]; | ||||
|         if (part == '.') { | ||||
|           parts.splice(i, 1); | ||||
|         } | ||||
|       } | ||||
|       return parts.join('/'); | ||||
|     }; | ||||
| 
 | ||||
|     var handler = new Untar(new Uint8Array(buf)); | ||||
|     handler.process(function(status, read, files, err) { | ||||
|       switch (status) { | ||||
|         case 'error': | ||||
|           failure(err); | ||||
|           break; | ||||
| 
 | ||||
|         case 'done': | ||||
|             var processed = []; | ||||
|             for (var i = 0; i < files.length; ++i) { | ||||
|               var currentFile = files[i]; | ||||
|               var path = collapsePath(currentFile.meta.filename); | ||||
| 
 | ||||
|               if (path == '' || path == 'pax_global_header') { continue; } | ||||
| 
 | ||||
|               processed.push({ | ||||
|                 'name': dataFileService.getName_(path), | ||||
|                 'path': path, | ||||
|                 'canRead': true, | ||||
|                 'toBlob': (function(currentFile) { | ||||
|                   return function() { | ||||
|                     return new Blob([currentFile.buffer], {type: 'application/octet-binary'}); | ||||
|                   }; | ||||
|                 }(currentFile)) | ||||
|               }); | ||||
|             } | ||||
|             success(processed); | ||||
|             break; | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   dataFileService.blobToString = function(blob, callback) { | ||||
|     var reader = new FileReader(); | ||||
|     reader.onload = function(event){ | ||||
|       callback(reader.result); | ||||
|     }; | ||||
|     reader.readAsText(blob); | ||||
|   }; | ||||
| 
 | ||||
|   dataFileService.arrayToString = function(buf, callback) { | ||||
|     var bb = new Blob([buf], {type: 'application/octet-binary'}); | ||||
|     var f = new FileReader(); | ||||
|     f.onload = function(e) { | ||||
|       callback(e.target.result); | ||||
|     }; | ||||
|     f.onerror = function(e) { | ||||
|       callback(null); | ||||
|     }; | ||||
|     f.onabort = function(e) { | ||||
|       callback(null); | ||||
|     }; | ||||
|     f.readAsText(bb); | ||||
|   }; | ||||
| 
 | ||||
|   dataFileService.readDataArrayAsPossibleArchive = function(buf, success, failure) { | ||||
|     dataFileService.tryAsZip_(buf, success, function() { | ||||
|       dataFileService.tryAsTarGz_(buf, success, function() { | ||||
|         dataFileService.tryAsTar_(buf, success, failure); | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   dataFileService.downloadDataFileAsArrayBuffer = function($scope, url, progress, error, loaded) { | ||||
|     var request = new XMLHttpRequest(); | ||||
|     request.open('GET', url, true); | ||||
|     request.responseType = 'arraybuffer'; | ||||
| 
 | ||||
|     request.onprogress = function(e) { | ||||
|       $scope.$apply(function() { | ||||
|         var percentLoaded; | ||||
|         if (e.lengthComputable) { | ||||
|           progress(e.loaded / e.total); | ||||
|         } | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     request.onerror = function() { | ||||
|       $scope.$apply(function() { | ||||
|         error(); | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     request.onload = function() { | ||||
|       if (this.status == 200) { | ||||
|         $scope.$apply(function() { | ||||
|           var uint8array = new Uint8Array(request.response); | ||||
|           loaded(uint8array); | ||||
|         }); | ||||
|         return; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     request.send(); | ||||
|   }; | ||||
| 
 | ||||
|   return dataFileService; | ||||
| }]); | ||||
							
								
								
									
										124
									
								
								static/js/services/datafile/datafile.service.impl.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								static/js/services/datafile/datafile.service.impl.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,124 @@ | |||
| import { DataFileServiceImpl } from './datafile.service.impl'; | ||||
| import { Mock } from 'ts-mocks'; | ||||
| import Spy = jasmine.Spy; | ||||
| 
 | ||||
| 
 | ||||
| describe("DataFileServiceImpl", () => { | ||||
|   var dataFileServiceImpl: DataFileServiceImpl; | ||||
|   var fileReaderMock: Mock<FileReader>; | ||||
|   var fileReader: FileReader; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     fileReaderMock = new Mock<FileReader>(); | ||||
|     fileReader = fileReaderMock.Object; | ||||
|     dataFileServiceImpl = new DataFileServiceImpl(() => fileReader); | ||||
|   }); | ||||
| 
 | ||||
|   describe("blobToString", () => { | ||||
|     var data: any; | ||||
|     var blob: Blob; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       data = {hello: "world"}; | ||||
|       blob = new Blob([JSON.stringify(data)]); | ||||
| 
 | ||||
|       fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { | ||||
|         fileReaderMock.Object.onload(<any>{target: {result: data}}); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls file reader to read given blob", (done) => { | ||||
|       dataFileServiceImpl.blobToString(blob, (result) => { | ||||
|         expect((<Spy>fileReader.readAsText).calls.argsFor(0)[0]).toEqual(blob); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls given callback with null if file reader errors", (done) => { | ||||
|       fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { | ||||
|         fileReaderMock.Object.onerror(new ErrorEvent("onerror")); | ||||
|       }); | ||||
| 
 | ||||
|       dataFileServiceImpl.blobToString(blob, (result) => { | ||||
|         expect(result).toBe(null); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls given callback with null if file reader aborts", (done) => { | ||||
|       fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { | ||||
|         fileReaderMock.Object.onabort(new Event("onabort")); | ||||
|       }); | ||||
| 
 | ||||
|       dataFileServiceImpl.blobToString(blob, (result) => { | ||||
|         expect(result).toBe(null); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls given callback with result when file reader successfully loads", (done) => { | ||||
|       dataFileServiceImpl.blobToString(blob, (result) => { | ||||
|         expect(result).toBe(data); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe("arrayToString", () => { | ||||
|     var blob: Blob; | ||||
|     var data: any; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       data = JSON.stringify({hello: "world"}); | ||||
|       blob = new Blob([data], {type: 'application/octet-binary'}); | ||||
| 
 | ||||
|       fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { | ||||
|         fileReaderMock.Object.onload(<any>{target: {result: data}}); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls file reader to read blob created from given buffer", (done) => { | ||||
|       dataFileServiceImpl.arrayToString(data, (result) => { | ||||
|         expect((<Spy>fileReader.readAsText).calls.argsFor(0)[0]).toEqual(blob); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls given callback with null if file reader errors", (done) => { | ||||
|       fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { | ||||
|         fileReaderMock.Object.onerror(new ErrorEvent("onerror")); | ||||
|       }); | ||||
| 
 | ||||
|       dataFileServiceImpl.arrayToString(data, (result) => { | ||||
|         expect(result).toEqual(null); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls given callback with null if file reader aborts", (done) => { | ||||
|       fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { | ||||
|         fileReaderMock.Object.onabort(new Event("onabort")); | ||||
|       }); | ||||
| 
 | ||||
|       dataFileServiceImpl.arrayToString(data, (result) => { | ||||
|         expect(result).toEqual(null); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls given callback with result when file reader successfully loads", (done) => { | ||||
|       dataFileServiceImpl.arrayToString(data, (result) => { | ||||
|         expect(result).toEqual(data); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe("readDataArrayAsPossibleArchive", () => { | ||||
|     // TODO
 | ||||
|   }); | ||||
| 
 | ||||
|   describe("downloadDataFileAsArrayBuffer", () => { | ||||
|     // TODO
 | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										182
									
								
								static/js/services/datafile/datafile.service.impl.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								static/js/services/datafile/datafile.service.impl.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,182 @@ | |||
| import { DataFileService } from './datafile.service'; | ||||
| import { Injectable } from 'angular-ts-decorators'; | ||||
| declare const JSZip: (buf: any) => void; | ||||
| declare const Zlib: any; | ||||
| declare const Untar: (uint8Array: Uint8Array) => void; | ||||
| 
 | ||||
| 
 | ||||
| @Injectable(DataFileService.name) | ||||
| export class DataFileServiceImpl implements DataFileService { | ||||
| 
 | ||||
|   constructor(private fileReaderFactory: () => FileReader) { | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   public blobToString(blob: Blob, callback: (result: string) => void): void { | ||||
|     var reader: FileReader = this.fileReaderFactory(); | ||||
|     reader.onload = (event: Event) => callback(event.target['result']); | ||||
|     reader.onerror = (event: Event) => callback(null); | ||||
|     reader.onabort = (event: Event) => callback(null); | ||||
|     reader.readAsText(blob); | ||||
|   } | ||||
| 
 | ||||
|   public arrayToString(buf: any, callback: (result: string) => void): void { | ||||
|     const blob: Blob = new Blob([buf], {type: 'application/octet-binary'}); | ||||
|     var reader: FileReader = this.fileReaderFactory(); | ||||
|     reader.onload = (event: Event) => callback(event.target['result']); | ||||
|     reader.onerror = (event: Event) => callback(null); | ||||
|     reader.onabort = (event: Event) => callback(null); | ||||
|     reader.readAsText(blob); | ||||
|   } | ||||
| 
 | ||||
|   public readDataArrayAsPossibleArchive(buf: any, | ||||
|                                         success: (result: any) => void, | ||||
|                                         failure: (error: any) => void): void { | ||||
|     this.tryAsZip(buf, success, () => { | ||||
|       this.tryAsTarGz(buf, success, () => { | ||||
|         this.tryAsTar(buf, success, failure); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   public downloadDataFileAsArrayBuffer($scope: ng.IScope, | ||||
|                                        url: string, | ||||
|                                        progress: (percent: number) => void, | ||||
|                                        error: () => void, | ||||
|                                        loaded: (uint8array: Uint8Array) => void): void { | ||||
|     var request: XMLHttpRequest = new XMLHttpRequest(); | ||||
|     request.open('GET', url, true); | ||||
|     request.responseType = 'arraybuffer'; | ||||
| 
 | ||||
|     request.onprogress = (e) => { | ||||
|       $scope.$apply(() => { | ||||
|         var percentLoaded; | ||||
|         if (e.lengthComputable) { | ||||
|           progress(e.loaded / e.total); | ||||
|         } | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     request.onerror = () => { | ||||
|       $scope.$apply(() => { | ||||
|         error(); | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     request.onload = function() { | ||||
|       if (request.status == 200) { | ||||
|         $scope.$apply(() => { | ||||
|           var uint8array = new Uint8Array(request.response); | ||||
|           loaded(uint8array); | ||||
|         }); | ||||
|         return; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     request.send(); | ||||
|   } | ||||
| 
 | ||||
|   private getName(filePath: string): string { | ||||
|     var parts: string[] = filePath.split('/'); | ||||
| 
 | ||||
|     return parts[parts.length - 1]; | ||||
|   } | ||||
| 
 | ||||
|   private tryAsZip(buf: any, success: (result: any) => void, failure: (error?: any) => void): void { | ||||
|     var zip = null; | ||||
|     var zipFiles = null; | ||||
|     try { | ||||
|       var zip = new JSZip(buf); | ||||
|       zipFiles = zip.files; | ||||
|     } catch (e) { | ||||
|       failure(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     var files = []; | ||||
|     for (var filePath in zipFiles) { | ||||
|       if (zipFiles.hasOwnProperty(filePath)) { | ||||
|         files.push({ | ||||
|           'name': this.getName(filePath), | ||||
|           'path': filePath, | ||||
|           'canRead': true, | ||||
|           'toBlob': (function(fp) { | ||||
|             return function() { | ||||
|               return new Blob([zip.file(fp).asArrayBuffer()]); | ||||
|             }; | ||||
|           }(filePath)) | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     success(files); | ||||
|   } | ||||
| 
 | ||||
|   private tryAsTarGz(buf: any, success: (result: any) => void, failure: (error?: any) => void): void { | ||||
|     var gunzip = new Zlib.Gunzip(new Uint8Array(buf)); | ||||
|     var plain = null; | ||||
| 
 | ||||
|     try { | ||||
|       plain = gunzip.decompress(); | ||||
|     } catch (e) { | ||||
|       failure(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (plain.byteLength == 0) { | ||||
|       plain = buf; | ||||
|     } | ||||
| 
 | ||||
|     this.tryAsTar(plain, success, failure); | ||||
|   } | ||||
| 
 | ||||
|   private tryAsTar(buf: any, success: (result: any) => void, failure: (error?: any) => void): void { | ||||
|     var collapsePath = function(originalPath) { | ||||
|       // Tar files can contain entries of the form './', so we need to collapse
 | ||||
|       // those paths down.
 | ||||
|       var parts = originalPath.split('/'); | ||||
|       for (var i = parts.length - 1; i >= 0; i--) { | ||||
|         var part = parts[i]; | ||||
|         if (part == '.') { | ||||
|           parts.splice(i, 1); | ||||
|         } | ||||
|       } | ||||
|       return parts.join('/'); | ||||
|     }; | ||||
| 
 | ||||
|     try { | ||||
|       var handler = new Untar(new Uint8Array(buf)); | ||||
|       handler.process((status, read, files, err) => { | ||||
|         switch (status) { | ||||
|           case 'error': | ||||
|             failure(err); | ||||
|             break; | ||||
| 
 | ||||
|           case 'done': | ||||
|             var processed = []; | ||||
|             for (var i = 0; i < files.length; ++i) { | ||||
|               var currentFile = files[i]; | ||||
|               var path = collapsePath(currentFile.meta.filename); | ||||
| 
 | ||||
|               if (path == '' || path == 'pax_global_header') { continue; } | ||||
| 
 | ||||
|               processed.push({ | ||||
|                 'name': this.getName(path), | ||||
|                 'path': path, | ||||
|                 'canRead': true, | ||||
|                 'toBlob': (function(currentFile) { | ||||
|                   return function() { | ||||
|                     return new Blob([currentFile.buffer], {type: 'application/octet-binary'}); | ||||
|                   }; | ||||
|                 }(currentFile)) | ||||
|               }); | ||||
|             } | ||||
|             success(processed); | ||||
|             break; | ||||
|         } | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       failure(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										48
									
								
								static/js/services/datafile/datafile.service.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								static/js/services/datafile/datafile.service.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | |||
| /** | ||||
|  * Service which provides helper methods for downloading a data file from a URL, and extracting | ||||
|  * its contents as .tar, .tar.gz, or .zip file. Note that this service depends on external | ||||
|  * library code in the lib/ directory: | ||||
|  *  - jszip.min.js | ||||
|  *  - Blob.js | ||||
|  *  - zlib.js | ||||
|  */ | ||||
| export abstract class DataFileService { | ||||
| 
 | ||||
|   /** | ||||
|    * Convert a blob to a string. | ||||
|    * @param blob The blob to convert. | ||||
|    * @param callback The success callback given converted blob. | ||||
|    */ | ||||
|   public abstract blobToString(blob: Blob, callback: (result: string) => void): void; | ||||
| 
 | ||||
|   /** | ||||
|    * Convert array to string. | ||||
|    * @param buf The array buffer to convert. | ||||
|    * @param callback The success callback given converted array buffer. | ||||
|    */ | ||||
|   public abstract arrayToString(buf: any, callback: (result: string) => void): void; | ||||
| 
 | ||||
|   /** | ||||
|    * Determine if a given data array is an archive file. | ||||
|    * @param buf The data array to check. | ||||
|    * @param success The success callback if the given array is an archive file, given the file contents. | ||||
|    * @param failure The failure callback if the given array is not an archive file, given the error message. | ||||
|    */ | ||||
|   public abstract readDataArrayAsPossibleArchive(buf: any, | ||||
|                                                  success: (result: any) => void, | ||||
|                                                  failure: (error: any) => void): void; | ||||
| 
 | ||||
|   /** | ||||
|    * Download a file into an array buffer while tracking progress. | ||||
|    * @param $scope An AngularJS $scope instance. | ||||
|    * @param url The URL of the file to be downloaded. | ||||
|    * @param progress The callback for download progress. | ||||
|    * @param error The error callback. | ||||
|    * @param loaded The success callback given the downloaded array buffer. | ||||
|    */ | ||||
|   public abstract downloadDataFileAsArrayBuffer($scope: ng.IScope, | ||||
|                                                 url: string, | ||||
|                                                 progress: (percent: number) => void, | ||||
|                                                 error: () => void, | ||||
|                                                 loaded: (uint8array: Uint8Array) => void): void; | ||||
| } | ||||
|  | @ -1,127 +0,0 @@ | |||
| /** | ||||
|  * Service which provides helper methods for extracting information out from a Dockerfile | ||||
|  * or an archive containing a Dockerfile. | ||||
|  */ | ||||
| angular.module('quay').factory('DockerfileService', ['DataFileService', 'Config', function(DataFileService, Config) { | ||||
|   var dockerfileService = {}; | ||||
| 
 | ||||
|   function DockerfileInfo(contents) { | ||||
|     this.contents = contents; | ||||
|   } | ||||
| 
 | ||||
|   DockerfileInfo.prototype.getRegistryBaseImage = function() { | ||||
|     var baseImage = this.getBaseImage(); | ||||
|     if (!baseImage) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     if (baseImage.indexOf(Config.getDomain() + '/') != 0) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     return baseImage.substring(Config.getDomain().length + 1); | ||||
|   }; | ||||
| 
 | ||||
|   DockerfileInfo.prototype.getBaseImage = function() { | ||||
|     var imageAndTag = this.getBaseImageAndTag(); | ||||
|     if (!imageAndTag) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     // Note, we have to handle a few different cases here:
 | ||||
|     // 1) someimage
 | ||||
|     // 2) someimage:tag
 | ||||
|     // 3) host:port/someimage
 | ||||
|     // 4) host:port/someimage:tag
 | ||||
|     var lastIndex = imageAndTag.lastIndexOf(':'); | ||||
|     if (lastIndex < 0) { | ||||
|       return imageAndTag; | ||||
|     } | ||||
| 
 | ||||
|     // Otherwise, check if there is a / in the portion after the split point. If so,
 | ||||
|     // then the latter is part of the path (and not a tag).
 | ||||
|     var afterColon = imageAndTag.substring(lastIndex + 1); | ||||
|     if (afterColon.indexOf('/') >= 0) { | ||||
|       return imageAndTag; | ||||
|     } | ||||
| 
 | ||||
|     return imageAndTag.substring(0, lastIndex); | ||||
|   }; | ||||
| 
 | ||||
|   DockerfileInfo.prototype.getBaseImageAndTag = function() { | ||||
|     var fromIndex = this.contents.indexOf('FROM '); | ||||
|     if (fromIndex < 0) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     var newline = this.contents.indexOf('\n', fromIndex); | ||||
|     if (newline < 0) { | ||||
|       newline = this.contents.length; | ||||
|     } | ||||
| 
 | ||||
|     return $.trim(this.contents.substring(fromIndex + 'FROM '.length, newline)); | ||||
|   }; | ||||
| 
 | ||||
|   DockerfileInfo.forData = function(contents) { | ||||
|     if (contents.indexOf('FROM ') < 0) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     return new DockerfileInfo(contents); | ||||
|   }; | ||||
| 
 | ||||
|   var processFiles = function(files, dataArray, success, failure) { | ||||
|     // The files array will be empty if the submitted file was not an archive. We therefore
 | ||||
|     // treat it as a single Dockerfile.
 | ||||
|     if (files.length == 0) { | ||||
|       DataFileService.arrayToString(dataArray, function(c) { | ||||
|         var result = DockerfileInfo.forData(c); | ||||
|         if (!result) { | ||||
|           failure('File chosen is not a valid Dockerfile'); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         success(result); | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     var found = false; | ||||
|     files.forEach(function(file) { | ||||
|       if (file['name'] == 'Dockerfile') { | ||||
|         DataFileService.blobToString(file.toBlob(), function(c) { | ||||
|           var result = DockerfileInfo.forData(c); | ||||
|           if (!result) { | ||||
|             failure('Dockerfile inside archive is not a valid Dockerfile'); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           success(result); | ||||
|         }); | ||||
|         found = true; | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     if (!found) { | ||||
|       failure('No Dockerfile found in root of archive'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   dockerfileService.getDockerfile = function(file, success, failure) { | ||||
|     var reader = new FileReader(); | ||||
|     reader.onload = function(e) { | ||||
|       var dataArray = reader.result; | ||||
|       DataFileService.readDataArrayAsPossibleArchive(dataArray, function(files) { | ||||
|         processFiles(files, dataArray, success, failure); | ||||
|       }, function() { | ||||
|         // Not an archive. Read directly as a single file.
 | ||||
|         processFiles([], dataArray, success, failure); | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     reader.onerror = failure; | ||||
|     reader.readAsArrayBuffer(file); | ||||
|   }; | ||||
| 
 | ||||
|   return dockerfileService; | ||||
| }]); | ||||
							
								
								
									
										292
									
								
								static/js/services/dockerfile/dockerfile.service.impl.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								static/js/services/dockerfile/dockerfile.service.impl.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,292 @@ | |||
| import { DockerfileServiceImpl, DockerfileInfoImpl } from './dockerfile.service.impl'; | ||||
| import { DataFileService } from '../datafile/datafile.service'; | ||||
| import Spy = jasmine.Spy; | ||||
| import { Mock } from 'ts-mocks'; | ||||
| 
 | ||||
| 
 | ||||
| describe("DockerfileServiceImpl", () => { | ||||
|   var dockerfileServiceImpl: DockerfileServiceImpl; | ||||
|   var dataFileServiceMock: Mock<DataFileService>; | ||||
|   var dataFileService: DataFileService; | ||||
|   var configMock: any; | ||||
|   var fileReaderMock: Mock<FileReader>; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     dataFileServiceMock = new Mock<DataFileService>(); | ||||
|     dataFileService = dataFileServiceMock.Object; | ||||
|     configMock = jasmine.createSpyObj('configMock', ['getDomain']); | ||||
|     fileReaderMock = new Mock<FileReader>(); | ||||
|     dockerfileServiceImpl = new DockerfileServiceImpl(dataFileService, configMock, () => fileReaderMock.Object); | ||||
|   }); | ||||
| 
 | ||||
|   describe("getDockerfile", () => { | ||||
|     var file: any; | ||||
|     var invalidArchiveFile: any[]; | ||||
|     var validArchiveFile: any[]; | ||||
|     var forDataSpy: Spy; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       file = "FROM quay.io/coreos/nginx:latest"; | ||||
|       validArchiveFile = [{name: 'Dockerfile', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue(file)}]; | ||||
|       invalidArchiveFile = [{name: 'main.exe', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue("")}]; | ||||
| 
 | ||||
|       dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { | ||||
|         failure([]); | ||||
|       }); | ||||
| 
 | ||||
|       dataFileServiceMock.setup(mock => mock.arrayToString).is((buf, callback) => callback("")); | ||||
| 
 | ||||
|       dataFileServiceMock.setup(mock => mock.blobToString).is((blob, callback) => callback(blob.toString())); | ||||
| 
 | ||||
|       forDataSpy = spyOn(DockerfileInfoImpl, "forData").and.returnValue(new DockerfileInfoImpl(file, configMock)); | ||||
| 
 | ||||
|       fileReaderMock.setup(mock => mock.readAsArrayBuffer).is((blob: Blob) => { | ||||
|         fileReaderMock.Object.onload(<any>{target: {result: file}}); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls datafile service to read given file as possible archive file", (done) => { | ||||
|       dockerfileServiceImpl.getDockerfile(file) | ||||
|         .then((dockerfile: DockerfileInfoImpl) => { | ||||
|           expect((<Spy>fileReaderMock.Object.readAsArrayBuffer).calls.argsFor(0)[0]).toEqual(file); | ||||
|           expect(dataFileService.readDataArrayAsPossibleArchive).toHaveBeenCalled(); | ||||
|           done(); | ||||
|         }) | ||||
|         .catch((error: string) => { | ||||
|           fail('Promise should be resolved'); | ||||
|           done(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls datafile service to convert file to string if given file is not an archive", (done) => { | ||||
|       dockerfileServiceImpl.getDockerfile(file) | ||||
|         .then((dockerfile: DockerfileInfoImpl) => { | ||||
|           expect((<Spy>dataFileService.arrayToString).calls.argsFor(0)[0]).toEqual(file); | ||||
|           done(); | ||||
|         }) | ||||
|         .catch((error: string) => { | ||||
|           fail('Promise should be resolved'); | ||||
|           done(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns rejected promise if given non-archive file that is not a valid Dockerfile", (done) => { | ||||
|       forDataSpy.and.returnValue(null); | ||||
|       dockerfileServiceImpl.getDockerfile(file) | ||||
|         .then((dockerfile: DockerfileInfoImpl) => { | ||||
|           fail("Promise should be rejected"); | ||||
|           done(); | ||||
|         }) | ||||
|         .catch((error: string) => { | ||||
|           expect(error).toEqual('File chosen is not a valid Dockerfile'); | ||||
|           done(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns resolved promise with new DockerfileInfoImpl instance if given valid Dockerfile", (done) => { | ||||
|       dockerfileServiceImpl.getDockerfile(file) | ||||
|         .then((dockerfile: DockerfileInfoImpl) => { | ||||
|           expect(dockerfile).toBeDefined(); | ||||
|           done(); | ||||
|         }) | ||||
|         .catch((error: string) => { | ||||
|           fail('Promise should be resolved'); | ||||
|           done(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns rejected promise if given archive file with no Dockerfile present in root directory", (done) => { | ||||
|       dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { | ||||
|         success(invalidArchiveFile); | ||||
|       }); | ||||
| 
 | ||||
|       dockerfileServiceImpl.getDockerfile(file) | ||||
|         .then((dockerfile: DockerfileInfoImpl) => { | ||||
|           fail('Promise should be rejected'); | ||||
|           done(); | ||||
|         }) | ||||
|         .catch((error: string) => { | ||||
|           expect(error).toEqual('No Dockerfile found in root of archive'); | ||||
|           done(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it("calls datafile service to convert blob to string if given file is an archive", (done) => { | ||||
|       dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { | ||||
|         success(validArchiveFile); | ||||
|       }); | ||||
| 
 | ||||
|       dockerfileServiceImpl.getDockerfile(file) | ||||
|         .then((dockerfile: DockerfileInfoImpl) => { | ||||
|           expect(validArchiveFile[0].toBlob).toHaveBeenCalled(); | ||||
|           expect((<Spy>dataFileService.blobToString).calls.argsFor(0)[0]).toEqual(validArchiveFile[0].toBlob()); | ||||
|           done(); | ||||
|         }) | ||||
|         .catch((error: string) => { | ||||
|           fail('Promise should be resolved'); | ||||
|           done(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns rejected promise if given archive file with invalid Dockerfile", (done) => { | ||||
|       forDataSpy.and.returnValue(null); | ||||
|       invalidArchiveFile[0].name = 'Dockerfile'; | ||||
|       dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { | ||||
|         success(invalidArchiveFile); | ||||
|       }); | ||||
| 
 | ||||
|       dockerfileServiceImpl.getDockerfile(file) | ||||
|         .then((dockerfile: DockerfileInfoImpl) => { | ||||
|           fail('Promise should be rejected'); | ||||
|           done(); | ||||
|         }) | ||||
|         .catch((error: string) => { | ||||
|           expect(error).toEqual('Dockerfile inside archive is not a valid Dockerfile'); | ||||
|           done(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns resolved promise of new DockerfileInfoImpl instance if given archive with valid Dockerfile", (done) => { | ||||
|       dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { | ||||
|         success(validArchiveFile); | ||||
|       }); | ||||
| 
 | ||||
|       dockerfileServiceImpl.getDockerfile(file) | ||||
|         .then((dockerfile: DockerfileInfoImpl) => { | ||||
|           expect(dockerfile).toBeDefined(); | ||||
|           done(); | ||||
|         }) | ||||
|         .catch((error: string) => { | ||||
|           fail('Promise should be resolved'); | ||||
|           done(); | ||||
|         }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| describe("DockerfileInfoImpl", () => { | ||||
|   var dockerfileInfoImpl: DockerfileInfoImpl; | ||||
|   var contents: string; | ||||
|   var configMock: any; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     contents = ""; | ||||
|     configMock = jasmine.createSpyObj('configMock', ['getDomain']); | ||||
|     dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock); | ||||
|   }); | ||||
| 
 | ||||
|   describe("forData", () => { | ||||
| 
 | ||||
|     it("returns null if given contents do not contain a 'FROM' command", () => { | ||||
|       expect(DockerfileInfoImpl.forData(contents, configMock)).toBe(null); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns a new DockerfileInfoImpl instance if given contents are valid", () => { | ||||
|       contents = "FROM quay.io/coreos/nginx"; | ||||
| 
 | ||||
|       expect(DockerfileInfoImpl.forData(contents, configMock) instanceof DockerfileInfoImpl).toBe(true); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe("getRegistryBaseImage", () => { | ||||
|     var domain: string; | ||||
|     var baseImage: string; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       domain = "quay.io"; | ||||
|       baseImage = "coreos/nginx"; | ||||
| 
 | ||||
|       configMock.getDomain.and.returnValue(domain); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns null if instance's contents do not contain a 'FROM' command", () => { | ||||
|       var getBaseImageSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(null); | ||||
| 
 | ||||
|       expect(dockerfileInfoImpl.getRegistryBaseImage()).toBe(null); | ||||
|       expect(getBaseImageSpy).toHaveBeenCalled(); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns null if the domain of the instance's config does not match that of the base image", () => { | ||||
|       configMock.getDomain.and.returnValue(domain); | ||||
|       spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue('host.com'); | ||||
| 
 | ||||
|       expect(dockerfileInfoImpl.getRegistryBaseImage()).toBe(null); | ||||
|       expect(configMock.getDomain).toHaveBeenCalled(); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns the registry base image", () => { | ||||
|       spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(`${domain}/${baseImage}`); | ||||
| 
 | ||||
|       expect(dockerfileInfoImpl.getRegistryBaseImage()).toEqual(baseImage); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe("getBaseImage", () => { | ||||
|     var host: string; | ||||
|     var port: number; | ||||
|     var tag: string; | ||||
|     var image: string; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       host = 'quay.io'; | ||||
|       port = 80; | ||||
|       tag = 'latest'; | ||||
|       image = 'coreos/nginx'; | ||||
|     }); | ||||
| 
 | ||||
|     it("returns null if instance's contents do not contain a 'FROM' command", () => { | ||||
|       var getBaseImageAndTagSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(null); | ||||
| 
 | ||||
|       expect(dockerfileInfoImpl.getBaseImage()).toBe(null); | ||||
|       expect(getBaseImageAndTagSpy).toHaveBeenCalled(); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns the image name if in the format 'someimage'", () => { | ||||
|       spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(image); | ||||
| 
 | ||||
|       expect(dockerfileInfoImpl.getBaseImage()).toEqual(image); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns the image name if in the format 'someimage:tag'", () => { | ||||
|       spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${image}:${tag}`); | ||||
| 
 | ||||
|       expect(dockerfileInfoImpl.getBaseImage()).toEqual(image); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns the host, port, and image name if in the format 'host:port/someimage'", () => { | ||||
|       spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${host}:${port}/${image}`); | ||||
| 
 | ||||
|       expect(dockerfileInfoImpl.getBaseImage()).toEqual(`${host}:${port}/${image}`); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns the host, port, and image name if in the format 'host:port/someimage:tag'", () => { | ||||
|       spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${host}:${port}/${image}:${tag}`); | ||||
| 
 | ||||
|       expect(dockerfileInfoImpl.getBaseImage()).toEqual(`${host}:${port}/${image}`); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe("getBaseImageAndTag", () => { | ||||
| 
 | ||||
|     it("returns null if instance's contents do not contain a 'FROM' command", () => { | ||||
|       expect(dockerfileInfoImpl.getBaseImageAndTag()).toBe(null); | ||||
|     }); | ||||
| 
 | ||||
|     it("returns a string containing the base image and tag from the instance's contents", () => { | ||||
|       contents = "FROM quay.io/coreos/nginx"; | ||||
|       dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock); | ||||
|       var baseImageAndTag: string = dockerfileInfoImpl.getBaseImageAndTag(); | ||||
| 
 | ||||
|       expect(baseImageAndTag).toEqual(contents.substring('FROM '.length, contents.length).trim()); | ||||
|     }); | ||||
| 
 | ||||
|     it("handles the presence of newlines", () => { | ||||
|       contents = "FROM quay.io/coreos/nginx\nRUN echo $0"; | ||||
|       dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock); | ||||
|       var baseImageAndTag: string = dockerfileInfoImpl.getBaseImageAndTag(); | ||||
| 
 | ||||
|       expect(baseImageAndTag).toEqual(contents.substring('FROM '.length, contents.indexOf('\n')).trim()); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										155
									
								
								static/js/services/dockerfile/dockerfile.service.impl.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								static/js/services/dockerfile/dockerfile.service.impl.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,155 @@ | |||
| import { DockerfileService, DockerfileInfo } from './dockerfile.service'; | ||||
| import { Injectable } from 'angular-ts-decorators'; | ||||
| import { DataFileService } from '../datafile/datafile.service'; | ||||
| 
 | ||||
| 
 | ||||
| @Injectable(DockerfileService.name) | ||||
| export class DockerfileServiceImpl implements DockerfileService { | ||||
| 
 | ||||
|   constructor(private DataFileService: DataFileService, | ||||
|               private Config: any, | ||||
|               private fileReaderFactory: () => FileReader) { | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   public getDockerfile(file: any): Promise<DockerfileInfoImpl | string> { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       var reader: FileReader = this.fileReaderFactory(); | ||||
|       reader.onload = (event: any) => { | ||||
|         this.DataFileService.readDataArrayAsPossibleArchive(event.target.result, | ||||
|           (files: any[]) => { | ||||
|             if (files.length > 0) { | ||||
|               this.processFiles(files) | ||||
|                 .then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo)) | ||||
|                 .catch((error: string) => reject(error)); | ||||
|             } | ||||
|             // Not an archive. Read directly as a single file.
 | ||||
|             else { | ||||
|               this.processFile(event.target.result) | ||||
|                 .then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo)) | ||||
|                 .catch((error: string) => reject(error)); | ||||
|             } | ||||
|           }, | ||||
|           () => { | ||||
|             // Not an archive. Read directly as a single file.
 | ||||
|             this.processFile(event.target.result) | ||||
|               .then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo)) | ||||
|               .catch((error: string) => reject(error)); | ||||
|           }); | ||||
|       }; | ||||
| 
 | ||||
|       reader.onerror = (event: any) => reject(event); | ||||
|       reader.readAsArrayBuffer(file); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private processFile(dataArray: any): Promise<DockerfileInfoImpl | string> { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       this.DataFileService.arrayToString(dataArray, (contents: string) => { | ||||
|         var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); | ||||
|         if (result == null) { | ||||
|           reject('File chosen is not a valid Dockerfile'); | ||||
|         } | ||||
|         else { | ||||
|           resolve(result); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private processFiles(files: any[]): Promise<DockerfileInfoImpl | string> { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       var found: boolean = false; | ||||
|       files.forEach((file) => { | ||||
|         if (file['name'] == 'Dockerfile') { | ||||
|           this.DataFileService.blobToString(file.toBlob(), (contents: string) => { | ||||
|             var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); | ||||
|             if (result == null) { | ||||
|               reject('Dockerfile inside archive is not a valid Dockerfile'); | ||||
|             } | ||||
|             else { | ||||
|               resolve(result); | ||||
|             } | ||||
|           }); | ||||
|           found = true; | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       if (!found) { | ||||
|         reject('No Dockerfile found in root of archive'); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class DockerfileInfoImpl implements DockerfileInfo { | ||||
| 
 | ||||
|   constructor(private contents: string, private config: any) { | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   public static forData(contents: string, config: any): DockerfileInfoImpl | null { | ||||
|     var dockerfileInfo: DockerfileInfoImpl = null; | ||||
|     if (contents.indexOf('FROM ') != -1) { | ||||
|       dockerfileInfo = new DockerfileInfoImpl(contents, config); | ||||
|     } | ||||
| 
 | ||||
|     return dockerfileInfo; | ||||
|   } | ||||
| 
 | ||||
|   public getRegistryBaseImage(): string | null { | ||||
|     var baseImage = this.getBaseImage(); | ||||
|     if (!baseImage) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     if (baseImage.indexOf(this.config.getDomain() + '/') != 0) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     return baseImage.substring(this.config.getDomain().length + 1); | ||||
|   } | ||||
| 
 | ||||
|   public getBaseImage(): string | null { | ||||
|     const imageAndTag = this.getBaseImageAndTag(); | ||||
|     if (!imageAndTag) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     // Note, we have to handle a few different cases here:
 | ||||
|     // 1) someimage
 | ||||
|     // 2) someimage:tag
 | ||||
|     // 3) host:port/someimage
 | ||||
|     // 4) host:port/someimage:tag
 | ||||
|     const lastIndex: number = imageAndTag.lastIndexOf(':'); | ||||
|     if (lastIndex == -1) { | ||||
|       return imageAndTag; | ||||
|     } | ||||
| 
 | ||||
|     // Otherwise, check if there is a / in the portion after the split point. If so,
 | ||||
|     // then the latter is part of the path (and not a tag).
 | ||||
|     const afterColon: string = imageAndTag.substring(lastIndex + 1); | ||||
|     if (afterColon.indexOf('/') != -1) { | ||||
|       return imageAndTag; | ||||
|     } | ||||
| 
 | ||||
|     return imageAndTag.substring(0, lastIndex); | ||||
|   } | ||||
| 
 | ||||
|   public getBaseImageAndTag(): string | null { | ||||
|     var baseImageAndTag: string = null; | ||||
| 
 | ||||
|     const fromIndex: number = this.contents.indexOf('FROM '); | ||||
|     if (fromIndex != -1) { | ||||
|       var newlineIndex: number = this.contents.indexOf('\n', fromIndex); | ||||
|       if (newlineIndex == -1) { | ||||
|         newlineIndex = this.contents.length; | ||||
|       } | ||||
| 
 | ||||
|       baseImageAndTag = this.contents.substring(fromIndex + 'FROM '.length, newlineIndex).trim(); | ||||
|     } | ||||
| 
 | ||||
|     return baseImageAndTag; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										38
									
								
								static/js/services/dockerfile/dockerfile.service.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								static/js/services/dockerfile/dockerfile.service.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | |||
| /** | ||||
|  * Service which provides helper methods for extracting information out from a Dockerfile | ||||
|  * or an archive containing a Dockerfile. | ||||
|  */ | ||||
| export abstract class DockerfileService { | ||||
| 
 | ||||
|   /** | ||||
|    * Retrieve Dockerfile from given file. | ||||
|    * @param file Dockerfile or archive file containing Dockerfile. | ||||
|    * @return promise Promise which resolves to new DockerfileInfo instance or rejects with error message. | ||||
|    */ | ||||
|   public abstract getDockerfile(file: any): Promise<DockerfileInfo | string>; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Model representing information about a specific Dockerfile. | ||||
|  */ | ||||
| export abstract class DockerfileInfo { | ||||
| 
 | ||||
|   /** | ||||
|    * Extract the registry base image from the Dockerfile contents. | ||||
|    * @return registryBaseImage The registry base image. | ||||
|    */ | ||||
|   public abstract getRegistryBaseImage(): string | null; | ||||
| 
 | ||||
|   /** | ||||
|    * Extract the base image from the Dockerfile contents. | ||||
|    * @return baseImage The base image. | ||||
|    */ | ||||
|   public abstract getBaseImage(): string | null; | ||||
| 
 | ||||
|   /** | ||||
|    * Extract the base image and tag from the Dockerfile contents. | ||||
|    * @return baseImageAndTag The base image and tag. | ||||
|    */ | ||||
|   public abstract getBaseImageAndTag(): string | null; | ||||
| } | ||||
|  | @ -9,14 +9,14 @@ describe("Service: PageServiceImpl", () => { | |||
|   }); | ||||
| 
 | ||||
|   describe("create", () => { | ||||
| 
 | ||||
|     // TODO
 | ||||
|   }); | ||||
| 
 | ||||
|   describe("get", () => { | ||||
| 
 | ||||
|     // TODO
 | ||||
|   }); | ||||
| 
 | ||||
|   describe("$get", () => { | ||||
| 
 | ||||
|     // TODO
 | ||||
|   }); | ||||
| }); | ||||
|  | @ -1,4 +1,6 @@ | |||
| import { ViewArrayImpl } from "static/js/services/view-array/view-array.impl"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Specialized wrapper around array which provides a toggle() method for viewing the contents of the | ||||
|  * array in a manner that is asynchronously filled in over a short time period. This prevents long | ||||
|  |  | |||
							
								
								
									
										6
									
								
								static/test/test-index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								static/test/test-index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| declare var require: any; | ||||
| 
 | ||||
| 
 | ||||
| // Require all modules ending in ".spec.ts" from the js directory and all subdirectories
 | ||||
| var testsContext = require.context("../js", true, /\.spec\.ts$/); | ||||
| testsContext.keys().forEach(testsContext); | ||||
		Reference in a new issue