Skip to content
This repository was archived by the owner on Jul 15, 2022. It is now read-only.

Commit a88b6c5

Browse files
author
Leonardo Chaia
committed
feat: adds registry cache and fallback to local daemon
1 parent 2d09fde commit a88b6c5

18 files changed

+414
-20
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"git-rev-sync": "^1.12.0",
8080
"hammerjs": "^2.0.8",
8181
"husky": "^1.1.2",
82+
"idb-keyval": "^3.1.0",
8283
"material-icons": "^0.2.3",
8384
"mousetrap": "^1.6.2",
8485
"npm-run-all": "^4.1.3",

src/app/app.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AppTabsModule } from './app-tabs.module';
77
import { GlobalShortcutsModule } from './global-shortcuts.module';
88
import { AppMenuModule } from './app-menu/app-menu.module';
99
import { JobsModule } from './jobs/jobs.module';
10+
import { TimCacheModule } from './tim-cache/tim-cache.module';
1011

1112
@NgModule({
1213
imports: [
@@ -17,6 +18,7 @@ import { JobsModule } from './jobs/jobs.module';
1718
AppTabsModule,
1819
AppMenuModule,
1920
GlobalShortcutsModule,
21+
TimCacheModule.forRoot(),
2022

2123
NavigationModule,
2224
],

src/app/daemon-tools/daemon-tools.module.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,14 @@ import { DockerImagesModule } from '../docker-images/docker-image.module';
120120
DockerSystemService,
121121
DockerContainerService,
122122
DockerVolumeService,
123+
124+
DockerDaemonImageSource,
123125
{
124126
provide: ImageSource,
125-
useClass: DockerDaemonImageSource,
127+
useExisting: DockerDaemonImageSource,
126128
multi: true
127-
}
129+
},
130+
128131
],
129132
entryComponents: [
130133
PullImageJobLogsComponent,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ImageSource, ImageListFilter, ImageListItemData, ImageInfo } from './image-source.model';
2+
import { TimCacheService } from '../tim-cache/tim-cache.service';
3+
import { Observable } from 'rxjs';
4+
import { ImageLayerHistoryV1Compatibility } from '../registry/registry.model';
5+
6+
export class CachingImageSource extends ImageSource {
7+
public get name() {
8+
return this.wrapped.name;
9+
}
10+
11+
public get registryDNS() {
12+
return this.wrapped.registryDNS;
13+
}
14+
15+
constructor(
16+
protected readonly wrapped: ImageSource,
17+
protected readonly cache: TimCacheService,
18+
protected readonly cacheDurationInMinutes: number) {
19+
super();
20+
this.priority = wrapped.priority;
21+
this.hasAuthentication = wrapped.hasAuthentication;
22+
this.credentials = wrapped.credentials;
23+
this.supportsDeletions = wrapped.supportsDeletions;
24+
}
25+
26+
public loadList(filter?: ImageListFilter): Observable<ImageListItemData[]> {
27+
return this.getFromCache(`imagesource/${this.registryDNS}/images/${JSON.stringify(filter)}`,
28+
() => this.wrapped.loadList(filter));
29+
}
30+
31+
public loadImageInfo(image: string): Observable<ImageInfo> {
32+
return this.getFromCache(`imagesource/${this.registryDNS}/${image}/info`,
33+
() => this.wrapped.loadImageInfo(image));
34+
}
35+
36+
public loadImageHistory(image: string): Observable<ImageLayerHistoryV1Compatibility[]> {
37+
return this.getFromCache(`imagesource/${this.registryDNS}/${image}/history`,
38+
() => this.wrapped.loadImageHistory(image));
39+
}
40+
41+
public loadImageTags(image: string): Observable<string[]> {
42+
return this.getFromCache(`imagesource/${this.registryDNS}/${image}/tags`,
43+
() => this.wrapped.loadImageTags(image));
44+
}
45+
46+
public isImageOwner(image: string) {
47+
return this.wrapped.isImageOwner(image);
48+
}
49+
50+
public isRegistryOwner(reg: string) {
51+
return this.wrapped.isRegistryOwner(reg);
52+
}
53+
54+
public deleteImage(image: string) {
55+
if (this.wrapped.deleteImage) {
56+
return this.wrapped.deleteImage(image);
57+
}
58+
}
59+
60+
public getBasicAuth() {
61+
if (this.wrapped.getBasicAuth) {
62+
return this.wrapped.getBasicAuth();
63+
}
64+
}
65+
66+
protected getFromCache<T>(key: string, fallback: () => Observable<T>): Observable<T> {
67+
if (typeof this.cacheDurationInMinutes === 'number' && this.cacheDurationInMinutes > 0) {
68+
return this.cache.get(key, fallback, this.cacheDurationInMinutes);
69+
} else {
70+
return fallback();
71+
}
72+
}
73+
}

src/app/docker-images/image-source.service.ts

-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export class ImageSourceService {
2424
switchMap(s => combineLatest(s.map(so => so.loadImageSources()))),
2525
map(arr => flatten(arr)),
2626
map(arr => arr.sort((a, b) => (a.priority > b.priority) ? 1 : ((b.priority > a.priority) ? -1 : 0))),
27-
// take(1),
2827
)
2928
.subscribe((srcs) => {
3029
this.sourcesSubject.next(srcs);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { ImageSource, ImageListFilter, ImageListItemData, ImageInfo } from '../docker-images/image-source.model';
2+
import { RegistryImageSource } from './registry.image-source';
3+
import { DockerDaemonImageSource } from '../daemon-tools/docker-daemon.image-source';
4+
import { Observable, throwError } from 'rxjs';
5+
import { ImageLayerHistoryV1Compatibility } from './registry.model';
6+
import { catchError } from 'rxjs/operators';
7+
8+
/**
9+
* Wraps a RegistryImageSource to use the Daemon as fallback
10+
* for image actions
11+
*/
12+
export class FallbackToLocalDaemonSource extends ImageSource {
13+
public get name() {
14+
return this.wrapped.name;
15+
}
16+
17+
public get registryDNS() {
18+
return this.wrapped.registryDNS;
19+
}
20+
21+
constructor(
22+
protected readonly wrapped: RegistryImageSource,
23+
protected readonly daemon: DockerDaemonImageSource) {
24+
super();
25+
this.priority = wrapped.priority;
26+
this.hasAuthentication = wrapped.hasAuthentication;
27+
this.credentials = wrapped.credentials;
28+
this.supportsDeletions = wrapped.supportsDeletions;
29+
}
30+
31+
public loadList(filter?: ImageListFilter): Observable<ImageListItemData[]> {
32+
return this.wrapped.loadList(filter);
33+
}
34+
35+
public loadImageInfo(image: string): Observable<ImageInfo> {
36+
return this.wrap(this.wrapped.loadImageInfo(image), () => this.daemon.loadImageInfo(image));
37+
}
38+
39+
public loadImageHistory(image: string): Observable<ImageLayerHistoryV1Compatibility[]> {
40+
return this.wrap(this.wrapped.loadImageHistory(image), () => this.daemon.loadImageHistory(image));
41+
}
42+
43+
public loadImageTags(image: string): Observable<string[]> {
44+
return this.wrap(this.wrapped.loadImageTags(image), () => this.daemon.loadImageTags(image));
45+
}
46+
47+
public isImageOwner(image: string) {
48+
return this.wrapped.isImageOwner(image);
49+
}
50+
51+
public isRegistryOwner(reg: string) {
52+
return this.wrapped.isRegistryOwner(reg);
53+
}
54+
55+
public getBasicAuth() {
56+
if (this.wrapped.getBasicAuth) {
57+
return this.wrapped.getBasicAuth();
58+
}
59+
}
60+
61+
protected wrap<T>(original: Observable<T>, fallback: () => Observable<T>): Observable<T> {
62+
return original
63+
.pipe(
64+
catchError(wrappedError =>
65+
fallback()
66+
.pipe(catchError(daemonError => throwError(wrappedError))))
67+
);
68+
}
69+
}

src/app/registry/registry-multiple.image-source.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
1-
import {
2-
ImageSource,
3-
ImageSourceMultiple
4-
} from '../docker-images/image-source.model';
1+
import { ImageSource, ImageSourceMultiple } from '../docker-images/image-source.model';
52
import { Observable } from 'rxjs';
63
import { Injectable } from '@angular/core';
74
import { map } from 'rxjs/operators';
85
import { SettingsService } from '../settings/settings.service';
96
import { RegistryService } from './registry.service';
10-
117
import { DockerHubImageSource } from '../docker-hub/docker-hub.image-source';
128
import { DockerHubService } from '../docker-hub/docker-hub.service';
139
import { DockerImageService } from '../daemon-tools/docker-image.service';
1410
import { RegistryImageSource } from './registry.image-source';
11+
import { DockerDaemonImageSource } from '../daemon-tools/docker-daemon.image-source';
12+
import { FallbackToLocalDaemonSource } from './fallback-to-local-daemon.image-source';
13+
import { CachingImageSource } from '../docker-images/caching-image-source';
14+
import { TimCacheService } from '../tim-cache/tim-cache.service';
1515

1616
@Injectable()
1717
export class RegistryImageSourceMultiple extends ImageSourceMultiple {
1818

1919
constructor(
2020
protected readonly settings: SettingsService,
2121
protected readonly registry: RegistryService,
22+
protected readonly daemonImageSource: DockerDaemonImageSource,
2223
protected readonly dockerImage: DockerImageService,
23-
protected readonly dockerHub: DockerHubService) {
24+
protected readonly dockerHub: DockerHubService,
25+
protected readonly cache: TimCacheService) {
2426
super();
2527
}
2628

@@ -29,12 +31,21 @@ export class RegistryImageSourceMultiple extends ImageSourceMultiple {
2931
.pipe(
3032
map(settings => settings.registries
3133
.map(r => {
34+
3235
// Create instances accordingly
36+
let source: RegistryImageSource;
3337
if (r.isDockerHub) {
34-
return new DockerHubImageSource(r, this.registry, this.dockerImage, this.dockerHub);
38+
source = new DockerHubImageSource(r, this.registry, this.dockerImage, this.dockerHub);
3539
} else {
36-
return new RegistryImageSource(r, this.registry);
40+
source = new RegistryImageSource(r, this.registry);
3741
}
42+
43+
// Wrap them with a local daemon fallback
44+
const fallback = new FallbackToLocalDaemonSource(source, this.daemonImageSource);
45+
46+
// Wrap them with Caching
47+
const cacheDuration = r.enableCaching ? r.cacheDurationInMinutes : null;
48+
return new CachingImageSource(fallback, this.cache, cacheDuration);
3849
}))
3950
);
4051
}

src/app/settings/registry-settings-modal/registry-settings-modal.component.html

+25
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,31 @@ <h1 mat-dialog-title>
2626
<p>
2727
This password is stored in plain text!
2828
</p>
29+
30+
<div fxLayout="row"
31+
fxLayoutAlign="start center"
32+
fxLayoutGap="8px">
33+
34+
<div fxFlex="30">
35+
<mat-checkbox formControlName='enableCaching'>
36+
Enable Cache
37+
</mat-checkbox>
38+
</div>
39+
40+
<div fxFlex="grow">
41+
<mat-form-field>
42+
<input type="number"
43+
matInput
44+
formControlName="cacheDurationInMinutes"
45+
placeholder="Cache Duration in Minutes">
46+
</mat-form-field>
47+
</div>
48+
</div>
49+
50+
<p *ngIf="registryFormGroup.value.enableCaching">
51+
Data from this registry will be cached for the amount of minutes you specify.
52+
</p>
53+
2954
</form>
3055

3156
<div style="margin:16px 0"

src/app/settings/registry-settings-modal/registry-settings-modal.component.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { RegistryService } from '../../registry/registry.service';
77
import { Observable, throwError } from 'rxjs';
88
import { take, catchError } from 'rxjs/operators';
99
import { HttpErrorResponse } from '@angular/common/http';
10+
import { DockerRegistrySettings } from '../settings.model';
1011

1112
@Component({
1213
selector: 'tim-registry-settings-modal',
@@ -33,9 +34,11 @@ export class RegistrySettingsModalComponent implements OnInit {
3334
this.registryFormGroup.disable();
3435
let obs: Observable<any>;
3536

36-
if (!this.registryFormGroup.value.url) {
37-
const username = this.registryFormGroup.value.username;
38-
const password = this.registryFormGroup.value.password;
37+
const registrySettings = this.registryFormGroup.getRawValue() as DockerRegistrySettings;
38+
39+
if (registrySettings.isDockerHub) {
40+
const username = registrySettings.username;
41+
const password = registrySettings.password;
3942
obs = this.dockerHub.logIn(username, password)
4043
.pipe(catchError((error: HttpErrorResponse) => {
4144
this.notification.open(error.error.detail || error.message, null, {
@@ -45,7 +48,7 @@ export class RegistrySettingsModalComponent implements OnInit {
4548
}));
4649

4750
} else {
48-
obs = this.registry.testRegistrySettings(this.registryFormGroup.value)
51+
obs = this.registry.testRegistrySettings(registrySettings)
4952
.pipe(catchError((error: HttpErrorResponse) => {
5053
this.notification.open(error.error || error.message, null, {
5154
panelClass: 'tim-bg-warn'
@@ -65,9 +68,11 @@ export class RegistrySettingsModalComponent implements OnInit {
6568
}, error => {
6669
this.loading = false;
6770
this.registryFormGroup.enable();
68-
if (!this.registryFormGroup.value.url) {
71+
72+
if (this.registryFormGroup.value.isDockerHub) {
6973
this.registryFormGroup.get('url').disable();
7074
}
75+
7176
console.error(error);
7277
});
7378
}

src/app/settings/settings-container/settings-container.component.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, OnInit, OnDestroy } from '@angular/core';
22
import { DockerRegistrySettings, DockerClientSettings } from '../settings.model';
3-
import { SettingsService, TIM_LOGO } from '../settings.service';
3+
import { SettingsService, TIM_LOGO, DEFAULT_REGISTRY_CACHE } from '../settings.service';
44
import { MatSnackBar } from '@angular/material';
55
import { FormBuilder, Validators, AbstractControl, FormGroup } from '@angular/forms';
66
import { Subject } from 'rxjs';
@@ -158,14 +158,31 @@ export class SettingsContainerComponent implements OnInit, OnDestroy {
158158
username: '',
159159
password: '',
160160
isDockerHub: false,
161+
enableCaching: true,
162+
cacheDurationInMinutes: DEFAULT_REGISTRY_CACHE
161163
};
162-
return this.fb.group({
164+
const formGroup = this.fb.group({
163165
'url': [{ value: registrySettings.url, disabled: registrySettings.isDockerHub },
164166
Validators.compose([Validators.required, Validators.pattern('https?://.+')])],
165167
'username': [registrySettings.username],
166168
'password': [registrySettings.password],
167169
'isDockerHub': [registrySettings.isDockerHub],
170+
'enableCaching': [registrySettings.enableCaching, Validators.required],
171+
'cacheDurationInMinutes': [registrySettings.cacheDurationInMinutes]
168172
});
173+
174+
formGroup.get('enableCaching')
175+
.valueChanges
176+
.subscribe(enabled => {
177+
const durationControl = formGroup.get('cacheDurationInMinutes');
178+
if (enabled) {
179+
durationControl.enable();
180+
} else {
181+
durationControl.disable();
182+
}
183+
});
184+
185+
return formGroup;
169186
}
170187

171188
private addRegistry(registrySettings?: DockerRegistrySettings) {

src/app/settings/settings.model.ts

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export interface DockerRegistrySettings {
1515
username: string;
1616
password: string;
1717
isDockerHub: boolean;
18+
enableCaching: boolean;
19+
cacheDurationInMinutes?: number;
1820
}
1921

2022
export interface ImageConfig {

0 commit comments

Comments
 (0)