Skip to content

Commit bec69b4

Browse files
authored
Merge pull request #2 from solunio/feat/user_input_credentials_loader
feat: add option to load docker registry credentials from cli
2 parents e203b5b + 2728f26 commit bec69b4

12 files changed

Lines changed: 581 additions & 101 deletions

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"cli-progress": "^3.9.0",
3333
"commander": "^8.1.0",
3434
"easy-table": "^1.1.1",
35+
"inquirer": "^8.1.2",
3536
"js-yaml": "^4.1.0",
3637
"lodash": "^4.17.11",
3738
"semver": "^7.3.5",
@@ -40,6 +41,7 @@
4041
"devDependencies": {
4142
"@types/cli-progress": "^3.9.2",
4243
"@types/easy-table": "^0.0.33",
44+
"@types/inquirer": "^7.3.3",
4345
"@types/js-yaml": "^4.0.3",
4446
"@types/lodash": "^4.14.172",
4547
"@types/node": "^12.20.21",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Credentials, CredentialsLoader } from '../credentials';
2+
3+
export interface DockerConfigAuthInformation {
4+
readonly auth: string;
5+
}
6+
7+
export interface DockerConfig {
8+
readonly auths: {
9+
[key: string]: DockerConfigAuthInformation;
10+
};
11+
}
12+
13+
class DockerAuthCredentials implements Credentials {
14+
constructor(private dockerAuth: DockerConfigAuthInformation) {}
15+
16+
public getToken(): string {
17+
return this.dockerAuth.auth;
18+
}
19+
}
20+
21+
export class DockerConfigCredentialsLoader implements CredentialsLoader {
22+
private readonly configCredentialsMap = new Map<string, DockerAuthCredentials>();
23+
24+
constructor(dockerConfig: DockerConfig) {
25+
for (const host of Object.keys(dockerConfig.auths)) {
26+
this.configCredentialsMap.set(host, new DockerAuthCredentials(dockerConfig.auths[host]));
27+
}
28+
}
29+
30+
public async loadCredentials(registryHost: string): Promise<Credentials | undefined> {
31+
return this.configCredentialsMap.get(registryHost);
32+
}
33+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { DockerConfig, DockerConfigCredentialsLoader } from './docker-config-credentials-loader';
2+
export { UserInputCredentialsLoader } from './user-input-credentials-loader';
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import inquirer from 'inquirer';
2+
3+
import { Credentials } from '../credentials';
4+
import { CredentialsLoader } from '../credentials/credentials-loader.interface';
5+
6+
export class LoginCredentials implements Credentials {
7+
constructor(private username: string, private password: string) {}
8+
9+
public getToken(): string {
10+
return Buffer.from(`${this.username}:${this.password}`, 'utf8').toString('base64');
11+
}
12+
}
13+
14+
export interface PromptCallbacks {
15+
onPromptStart(): void;
16+
onPromptStop(): void;
17+
}
18+
19+
export class UserInputCredentialsLoader implements CredentialsLoader {
20+
constructor(private readonly progressInterruptor?: PromptCallbacks) {}
21+
22+
public async loadCredentials(registryHost: string): Promise<Credentials | undefined> {
23+
this.progressInterruptor?.onPromptStart();
24+
25+
const response = await inquirer.prompt<{ username: string; password: string }>([
26+
{
27+
type: 'input',
28+
name: 'username',
29+
message: `Username for docker registry '${registryHost}':`
30+
},
31+
{
32+
type: 'password',
33+
name: 'password',
34+
message: `Password:`
35+
}
36+
]);
37+
38+
this.progressInterruptor?.onPromptStop();
39+
40+
return new LoginCredentials(response.username, response.password);
41+
}
42+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Credentials } from './credentials.interface';
2+
3+
export interface CredentialsLoader {
4+
loadCredentials(registryHost: string): Promise<Credentials | undefined>;
5+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { CredentialsLoader } from './credentials-loader.interface';
2+
import { Credentials } from './credentials.interface';
3+
4+
export class CredentialsStore {
5+
private store = new Map<string, Credentials>();
6+
7+
constructor(private readonly credentialsLoaders: CredentialsLoader[]) {}
8+
9+
public async getCredentials(registryHost: string | undefined): Promise<Credentials | undefined> {
10+
if (!registryHost) return;
11+
12+
const credentials = this.store.get(registryHost);
13+
if (credentials != null) return credentials;
14+
15+
for (const loader of this.credentialsLoaders) {
16+
const loadedCredentials = await loader.loadCredentials(registryHost);
17+
if (loadedCredentials != null) {
18+
this.addCredentials(registryHost, loadedCredentials);
19+
return loadedCredentials;
20+
}
21+
}
22+
23+
return undefined;
24+
}
25+
26+
public addCredentials(registryHost: string, credentials: Credentials): void {
27+
this.store.set(registryHost, credentials);
28+
}
29+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Credentials {
2+
getToken(): string;
3+
}

src/lib/credentials/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { Credentials } from './credentials.interface';
2+
export { CredentialsLoader } from './credentials-loader.interface';
3+
export { CredentialsStore } from './credentials-store';

src/lib/docker-utils.ts

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import axios from 'axios';
22
import { last as _last, merge as _merge } from 'lodash';
3-
import { resolve as resolvePath } from 'path';
43
import { compare as semverCompare, maxSatisfying as semverMaxSatisfying, valid as semverValid } from 'semver';
54

6-
import { readFile } from './utils';
5+
import { Credentials, CredentialsStore } from './credentials';
76

87
export const DOCKER_REGISTRY_HOST = 'docker.io';
98

@@ -13,10 +12,6 @@ export interface DockerImage {
1312
host?: string;
1413
}
1514

16-
export interface Credentials {
17-
getToken(): string;
18-
}
19-
2015
interface TokenChellange {
2116
realm: string;
2217
service: string;
@@ -126,18 +121,17 @@ export function parseDockerImage(imageString: string): DockerImage {
126121
}
127122

128123
export async function listTags(credentialsStore: CredentialsStore, dockerImage: DockerImage): Promise<string[]> {
129-
const credentials = credentialsStore.getCredentials(dockerImage.host);
130124
const result = await requestNew<{ tags: string[] }>(
131125
`https://${dockerImage.host}/v2/${dockerImage.name}/tags/list`,
132-
credentials
126+
await credentialsStore.getCredentials(dockerImage.host)
133127
);
134128
return result.tags;
135129
}
136130

137131
export async function listRepositories(registryHost: string, credentialsStore: CredentialsStore): Promise<string[]> {
138132
const result = await requestNew<{ repositories: string[] }>(
139133
`https://${registryHost}/v2/_catalog`,
140-
credentialsStore.getCredentials(registryHost)
134+
await credentialsStore.getCredentials(registryHost)
141135
);
142136
return result.repositories;
143137
}
@@ -172,35 +166,3 @@ export async function getImageUpdateTags(
172166

173167
return { wanted, latest };
174168
}
175-
176-
export async function readDockerConfig(dockerConfigPath: string): Promise<any> {
177-
const data = await readFile(resolvePath(dockerConfigPath));
178-
return JSON.parse(data);
179-
}
180-
181-
class DockerAuthCredentials implements Credentials {
182-
constructor(private dockerAuth: any) {}
183-
184-
public getToken(): string {
185-
return this.dockerAuth.auth;
186-
}
187-
}
188-
189-
export class CredentialsStore {
190-
private store = new Map<string, Credentials>();
191-
192-
constructor(dockerConfig: any) {
193-
for (const host of Object.keys(dockerConfig.auths)) {
194-
this.addCredentials(host, new DockerAuthCredentials(dockerConfig.auths[host]));
195-
}
196-
}
197-
198-
public getCredentials(registryHost: string | undefined): Credentials | undefined {
199-
if (!registryHost) return;
200-
return this.store.get(registryHost);
201-
}
202-
203-
public addCredentials(registryHost: string, credentials: Credentials): void {
204-
this.store.set(registryHost, credentials);
205-
}
206-
}

src/lib/index.ts

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,23 @@ import { Bar as CliProgressBar, Presets as CliProgressBarPresets } from 'cli-pro
22
import EasyTable from 'easy-table';
33
import { diff as semverDiff, valid as semverValid } from 'semver';
44

5+
import { CredentialsLoader, CredentialsStore } from './credentials';
6+
import {
7+
DockerConfig,
8+
DockerConfigCredentialsLoader,
9+
UserInputCredentialsLoader
10+
} from './credentials-loader-implementations';
511
import { getComposeImages } from './compose-utils';
612
import {
7-
Credentials,
8-
CredentialsStore,
913
DOCKER_REGISTRY_HOST,
1014
DockerImage,
1115
getImageUpdateTags,
1216
getLatestImageVersion,
13-
listRepositories,
14-
readDockerConfig
17+
listRepositories
1518
} from './docker-utils';
19+
import { readJsonFile } from './utils';
1620

1721
// only for testing purposes
18-
class LoginCredentials implements Credentials {
19-
constructor(private username: string, private password: string) {}
20-
21-
public getToken(): string {
22-
return Buffer.from(`${this.username}:${this.password}`, 'utf8').toString('base64');
23-
}
24-
}
2522

2623
async function listLatestImageVersions(
2724
registryHost: string,
@@ -62,9 +59,64 @@ export interface OutdatedImage {
6259
latestVersion: string;
6360
}
6461

62+
class ProgressBarWrapper {
63+
private readonly progressBar: CliProgressBar;
64+
65+
private progressInformation: { total: number; current: number } | undefined;
66+
67+
constructor() {
68+
this.progressBar = new CliProgressBar({}, CliProgressBarPresets.shades_classic);
69+
}
70+
71+
public start(total: number, initialValue: number): void {
72+
this.progressInformation = { total, current: initialValue };
73+
this.progressBar.start(total, initialValue);
74+
}
75+
76+
public stop(): void {
77+
this.progressBar.stop();
78+
}
79+
80+
public pause(): void {
81+
this.progressBar.stop();
82+
}
83+
84+
public resume(): void {
85+
if (this.progressInformation == null) throw new Error('Progressbar was never started');
86+
this.progressBar.start(this.progressInformation.total, this.progressInformation.current);
87+
}
88+
89+
public increment(inc: number): void {
90+
if (this.progressInformation == null) throw new Error('Progressbar was never started');
91+
this.progressInformation = {
92+
total: this.progressInformation.total,
93+
current: this.progressInformation.current + inc
94+
};
95+
96+
this.progressBar.increment(inc);
97+
}
98+
}
99+
65100
export async function listOutdated(options: Options): Promise<OutdatedImage[]> {
66-
const config = await readDockerConfig(options.dockerConfigPath);
67-
const credentials = new CredentialsStore(config);
101+
const progressBar = new ProgressBarWrapper();
102+
103+
const credentialsLoaders: CredentialsLoader[] = [];
104+
105+
try {
106+
const config = await readJsonFile<DockerConfig>(options.dockerConfigPath);
107+
credentialsLoaders.push(new DockerConfigCredentialsLoader(config));
108+
} catch (err) {
109+
console.error('Error while trying to add credentials-loader from docker config file. Skipping...', err);
110+
}
111+
112+
credentialsLoaders.push(
113+
new UserInputCredentialsLoader({
114+
onPromptStart: () => progressBar.pause(),
115+
onPromptStop: () => progressBar.resume()
116+
})
117+
);
118+
119+
const credentials = new CredentialsStore(credentialsLoaders);
68120
// const res = await listLatestImageVersions('docker.solunio.com', credentials, 'common')
69121
// console.log('res: ', res);
70122

@@ -87,7 +139,6 @@ export async function listOutdated(options: Options): Promise<OutdatedImage[]> {
87139
filteredImages.push(composeImage);
88140
}
89141
const outdatedImages: OutdatedImage[] = [];
90-
const progressBar = new CliProgressBar({}, CliProgressBarPresets.shades_classic);
91142
progressBar.start(filteredImages.length, 0);
92143

93144
try {

0 commit comments

Comments
 (0)