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

Commit 33c8948

Browse files
author
Leonardo Chaia
committed
feat: adds registry image history view
1 parent ba176fc commit 33c8948

18 files changed

+286
-3
lines changed

src/app/app-tabs.module.ts

+7
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
} from './application-templates/application-edit-list-container/application-edit-list-container.component';
3131
import { ContainerLogsContainerComponent } from './daemon-tools/container-logs-container/container-logs-container.component';
3232
import { DockerHubRepoListContainerComponent } from './docker-hub/docker-hub-repo-list-container/docker-hub-repo-list-container.component';
33+
import { ImagePreviewContainerComponent } from './docker-image-preview/image-preview-container/image-preview-container.component';
34+
import { DockerImagePreviewModule } from './docker-image-preview/docker-image-preview.module';
3335

3436
const TIMONEER_AVAILABLE_TABS: TabConfiguration[] = [
3537
{
@@ -101,6 +103,10 @@ const TIMONEER_AVAILABLE_TABS: TabConfiguration[] = [
101103
id: TimoneerTabs.DOCKERHUB_IMAGES,
102104
component: DockerHubRepoListContainerComponent,
103105
},
106+
{
107+
id: TimoneerTabs.IMAGE_PREVIEW,
108+
component: ImagePreviewContainerComponent,
109+
},
104110
{
105111
id: TimoneerTabs.DASHBOARD,
106112
title: 'Dashboard',
@@ -120,6 +126,7 @@ const TIMONEER_AVAILABLE_TABS: TabConfiguration[] = [
120126
ApplicationTemplatesModule,
121127
RegistryModule,
122128
SettingsModule,
129+
DockerImagePreviewModule,
123130
TabsModule.forRoot(TIMONEER_AVAILABLE_TABS),
124131
],
125132
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { NgModule } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { ImagePreviewContainerComponent } from './image-preview-container/image-preview-container.component';
4+
import { RegistryModule } from '../registry/registry.module';
5+
import { MatCardModule, MatIconModule } from '@angular/material';
6+
import { ImageHistoryComponent } from './image-history/image-history.component';
7+
import { NgObjectPipesModule } from 'angular-pipes';
8+
import { ManifestMetadataComponent } from './manifest-metadata/manifest-metadata.component';
9+
10+
@NgModule({
11+
imports: [
12+
CommonModule,
13+
RegistryModule,
14+
NgObjectPipesModule,
15+
MatCardModule,
16+
MatIconModule,
17+
],
18+
declarations: [
19+
ImagePreviewContainerComponent,
20+
ImageHistoryComponent,
21+
ManifestMetadataComponent,
22+
]
23+
})
24+
export class DockerImagePreviewModule { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<mat-card *ngFor="let row of history"
2+
style="position:relative">
3+
<code *ngFor="let cmd of row.container_config.Cmd">
4+
{{getCommand(cmd)}}
5+
</code>
6+
7+
<div style="position: absolute; bottom: 2px; right: 4px;">
8+
<small>
9+
{{row.os}} {{row.architecture}}
10+
{{row.created|date:'short'}}
11+
</small>
12+
</div>
13+
</mat-card>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mat-card {
2+
margin-bottom: 16px;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Component, OnInit, Input } from '@angular/core';
2+
import { ImageManifest, ImageLayerHistoryV1Compatibility } from '../../registry/registry.model';
3+
4+
@Component({
5+
selector: 'tim-image-history',
6+
templateUrl: './image-history.component.html',
7+
styleUrls: ['./image-history.component.scss']
8+
})
9+
export class ImageHistoryComponent implements OnInit {
10+
11+
@Input()
12+
public manifest: ImageManifest;
13+
14+
public history: ImageLayerHistoryV1Compatibility[];
15+
16+
constructor() { }
17+
18+
public ngOnInit() {
19+
this.history = this.manifest.history.map(h => JSON.parse(h.v1Compatibility) as ImageLayerHistoryV1Compatibility);
20+
}
21+
22+
public getCommand(cmd: string) {
23+
const nop = '#(nop)';
24+
const index = cmd.indexOf(nop);
25+
if (index >= 0) {
26+
return cmd.slice(index + nop.length);
27+
} else {
28+
return cmd;
29+
}
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div fxLayout="column"
2+
style="padding:16px;">
3+
4+
<div *ngIf="manifest">
5+
<tim-manifest-metadata [manifest]="manifest"></tim-manifest-metadata>
6+
7+
<h2 style="text-align: center">
8+
History
9+
</h2>
10+
11+
<tim-image-history [manifest]="manifest"></tim-image-history>
12+
</div>
13+
</div>

src/app/docker-image-preview/image-preview-container/image-preview-container.component.scss

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Component, OnInit, Inject } from '@angular/core';
2+
import { RegistryService } from '../../registry/registry.service';
3+
import { TAB_DATA } from '../../tabs/tab.model';
4+
import { ImageManifest } from '../../registry/registry.model';
5+
6+
export interface ImagePreviewContainerComponentData {
7+
image: string;
8+
tag: string;
9+
registry: string;
10+
}
11+
12+
@Component({
13+
selector: 'tim-image-preview-container',
14+
templateUrl: './image-preview-container.component.html',
15+
styleUrls: ['./image-preview-container.component.scss']
16+
})
17+
export class ImagePreviewContainerComponent implements OnInit {
18+
public manifest: ImageManifest;
19+
20+
constructor(
21+
@Inject(TAB_DATA)
22+
private tabData: ImagePreviewContainerComponentData,
23+
private registry: RegistryService) { }
24+
25+
public ngOnInit() {
26+
this.registry.getImageManifest(this.tabData.registry, this.tabData.image, this.tabData.tag)
27+
.subscribe(manifest => {
28+
this.manifest = manifest;
29+
});
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<mat-card>
2+
<h2>
3+
<mat-icon class="tim-text-primary">iso</mat-icon>
4+
{{manifest.name}}:{{manifest.tag}}
5+
</h2>
6+
<p *ngIf="history[0].author">
7+
Author: {{history[0].author}}
8+
</p>
9+
<p *ngIf="history[0].created">
10+
Created: {{history[0].created | date:'short'}}
11+
</p>
12+
<p *ngIf="history[0].os">
13+
OS: {{history[0].os}} {{history[0].architecture}}
14+
</p>
15+
<p *ngIf="history[0].docker_version">
16+
Built with Docker v{{history[0].docker_version}}
17+
</p>
18+
</mat-card>
19+
20+
<mat-card *ngIf="baseConfig.Labels && (baseConfig.Labels|keys).length">
21+
<h3>
22+
<mat-icon class="tim-text-primary">label</mat-icon>
23+
Labels
24+
</h3>
25+
<div *ngFor="let label of baseConfig.Labels | keyvalue">
26+
<b>{{label.key | titlecase}}</b>
27+
{{label.value}}
28+
</div>
29+
</mat-card>
30+
31+
<mat-card *ngIf="baseConfig.Env && baseConfig.Env.length">
32+
<h3>
33+
<mat-icon class="tim-text-primary">confirmation_number</mat-icon>
34+
Environment Variables
35+
</h3>
36+
<pre><ng-container *ngFor="let env of baseConfig.Env">{{env+"\n"}}</ng-container></pre>
37+
</mat-card>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
mat-card {
2+
margin-bottom: 16px;
3+
}
4+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Component, OnInit, Input } from '@angular/core';
2+
import { ImageManifest, ImageLayerHistoryV1Compatibility, ImageHistoryContainerConfig } from '../../registry/registry.model';
3+
4+
@Component({
5+
selector: 'tim-manifest-metadata',
6+
templateUrl: './manifest-metadata.component.html',
7+
styleUrls: ['./manifest-metadata.component.scss']
8+
})
9+
export class ManifestMetadataComponent implements OnInit {
10+
11+
@Input()
12+
public manifest: ImageManifest;
13+
public history: ImageLayerHistoryV1Compatibility[];
14+
public baseConfig: ImageHistoryContainerConfig;
15+
16+
constructor() { }
17+
18+
public ngOnInit() {
19+
this.history = this.manifest.history.map(h => JSON.parse(h.v1Compatibility) as ImageLayerHistoryV1Compatibility);
20+
this.baseConfig = this.history[0].container_config;
21+
}
22+
23+
}

src/app/registry/registry-auth.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class RegistryAuthService {
2525
password: string) {
2626

2727
const authHeader = btoa(`${username}:${password}`);
28-
return this.httpClient.post<{ token: string }>(url, null, {
28+
return this.httpClient.get<{ token: string }>(url, {
2929
params: {
3030
service: service,
3131
scope: scope,

src/app/registry/registry-list/registry-list.component.html

+7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ <h2>
2525
</h2>
2626
</div>
2727

28+
29+
<div>
30+
<a mat-raised-button
31+
(click)="imagePreview(repo)">
32+
+info
33+
</a>
34+
</div>
2835
<div>
2936
<a mat-raised-button
3037
color="primary"

src/app/registry/registry-list/registry-list.component.ts

+12
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SettingsService } from '../../settings/settings.service';
55
import { map } from 'rxjs/operators';
66
import { TabService } from '../../tabs/tab.service';
77
import { TimoneerTabs } from '../../timoneer-tabs';
8+
import { ImagePreviewContainerComponentData } from '../../docker-image-preview/image-preview-container/image-preview-container.component';
89

910
@Component({
1011
selector: 'tim-registry-list',
@@ -58,4 +59,15 @@ export class RegistryListComponent implements OnInit {
5859
});
5960
}
6061

62+
public imagePreview(repo: string) {
63+
this.tabService.add(TimoneerTabs.IMAGE_PREVIEW, {
64+
title: repo,
65+
params: {
66+
image: repo,
67+
registry: this.registryUrl,
68+
tag: 'latest'
69+
} as ImagePreviewContainerComponentData
70+
});
71+
}
72+
6173
}

src/app/registry/registry.model.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
2+
export interface FsLayer {
3+
blobSum: string;
4+
}
5+
6+
export interface ImageLayerHistory {
7+
v1Compatibility: string;
8+
}
9+
10+
export interface Jwk {
11+
crv: string;
12+
kid: string;
13+
kty: string;
14+
x: string;
15+
y: string;
16+
}
17+
18+
export interface SignatureHeader {
19+
jwk: Jwk;
20+
alg: string;
21+
}
22+
23+
export interface Signature {
24+
header: SignatureHeader;
25+
signature: string;
26+
protected: string;
27+
}
28+
29+
export interface ImageManifest {
30+
schemaVersion: number;
31+
name: string;
32+
tag: string;
33+
architecture: string;
34+
fsLayers: FsLayer[];
35+
history: ImageLayerHistory[];
36+
signatures: Signature[];
37+
}
38+
39+
export interface ImageHistoryContainerConfig {
40+
Hostname: string;
41+
Domainname: string;
42+
User: string;
43+
AttachStdin: boolean;
44+
AttachStdout: boolean;
45+
AttachStderr: boolean;
46+
ExposedPorts: { [key: string]: any };
47+
Tty: boolean;
48+
OpenStdin: boolean;
49+
StdinOnce: boolean;
50+
Env: string[];
51+
Cmd: string[];
52+
ArgsEscaped: boolean;
53+
Image: string;
54+
Volumes?: any;
55+
WorkingDir: string;
56+
Entrypoint: string[];
57+
OnBuild: any[];
58+
Labels: { [key: string]: string };
59+
}
60+
61+
export interface ImageLayerHistoryV1Compatibility {
62+
architecture: string;
63+
config: ImageHistoryContainerConfig;
64+
container: string;
65+
container_config: ImageHistoryContainerConfig;
66+
created: string;
67+
docker_version: string;
68+
id: string;
69+
os: string;
70+
parent: string;
71+
throwaway?: boolean;
72+
author: string;
73+
}

src/app/registry/registry.service.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { throwError, Observable, forkJoin, of } from 'rxjs';
55
import { SettingsService } from '../settings/settings.service';
66
import { DockerRegistrySettings } from '../settings/settings.model';
77
import { RegistryAuthService } from './registry-auth.service';
8+
import { ImageManifest } from './registry.model';
89

910
@Injectable({
1011
providedIn: 'root'
@@ -27,7 +28,7 @@ export class RegistryService {
2728
}
2829

2930
public getImageManifest(registryUrl: string, name: string, reference: string) {
30-
return this.get(registryUrl, `v2/${name}/manifests/${reference}`);
31+
return this.get<ImageManifest>(registryUrl, `v2/${name}/manifests/${reference}`);
3132
}
3233

3334
public getRepositoriesFromAllRegistries() {

src/app/settings/settings.service.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class SettingsService {
3636
} else {
3737
this.settingsSubject.next(<ApplicationSettings>{
3838
registries: [{
39-
url: 'https://docker.io/',
39+
url: 'https://index.docker.io/',
4040
allowsCatalog: false,
4141
editable: false,
4242
}],
@@ -68,6 +68,8 @@ export class SettingsService {
6868
for (const registry of settings.registries) {
6969
if (registry.url) {
7070
registry.url = this.ensureEndingSlash(registry.url);
71+
} else {
72+
registry.url = 'https://index.docker.io/';
7173
}
7274
}
7375
}

src/app/timoneer-tabs.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ export class TimoneerTabs {
1515
public static APPLICATION_LAUNCH = 'application-launch';
1616
public static REGISTRY_IMAGES = 'registry-images';
1717
public static DOCKERHUB_IMAGES = 'dockerhub-images';
18+
public static IMAGE_PREVIEW = 'image-preview';
1819
public static SETTINGS = 'settings';
1920
}

0 commit comments

Comments
 (0)