Skip to content

Commit

Permalink
feat: add custom registries support, closes #61
Browse files Browse the repository at this point in the history
  • Loading branch information
YuukanOO committed May 27, 2024
1 parent b4f6490 commit bdb2484
Show file tree
Hide file tree
Showing 60 changed files with 2,006 additions and 134 deletions.
42 changes: 41 additions & 1 deletion api.http
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ Content-Type: application/json
{
"name": "docker outside",
"url": "http://docker.localhost",
"docker": {}
"docker": {

}
}

###
Expand All @@ -76,6 +78,44 @@ GET {{url}}/targets/{{createTarget.response.body.$.id}}

DELETE {{url}}/targets/{{createTarget.response.body.$.id}}

###

GET {{url}}/registries

###

# @name createRegistry

POST {{url}}/registries
Content-Type: application/json

{
"name": "Local registry",
"url": "http://localhost:5000"
}

###

PATCH {{url}}/registries/{{createRegistry.response.body.$.id}}
Content-Type: application/json

{
"name": "Local registry",
"url": "http://localhost:5001",
"credentials": {
"username": "admin",
"password": "admin"
}
}

###

DELETE {{url}}/registries/{{createRegistry.response.body.$.id}}

###

GET {{url}}/registries/{{createRegistry.response.body.$.id}}

###
# @name createApp

Expand Down
2 changes: 1 addition & 1 deletion cmd/serve/front/src/components/data-table.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<tbody>
{#if !data || data.length === 0}
<tr>
<td colspan={columns.length}>
<td colspan={columns.length + ($$slots.expanded ? 1 : 0)}>
<BlankSlate>
<p>{l.translate('datatable.no_data')}</p>
</BlankSlate>
Expand Down
30 changes: 30 additions & 0 deletions cmd/serve/front/src/components/registry-card.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import Card from '$components/card.svelte';
import Link from '$components/link.svelte';
import routes from '$lib/path';
import type { Registry } from '$lib/resources/registries';
export let data: Registry;
</script>

<Card>
<h2 class="title"><Link href={routes.editRegistry(data.id)}>{data.name}</Link></h2>
<div class="meta">{data.url}</div>
</Card>

<style module>
.title {
color: var(--co-text-5);
font: var(--ty-heading-2);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.meta {
font: var(--ty-caption);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
</style>
7 changes: 6 additions & 1 deletion cmd/serve/front/src/lib/fetcher/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ export default class CacheFetchService implements FetchService {
const result = await api<TOut, TIn>(method, url, body, options);

// Invalidate all the cache entries that matches the base key
const keys = [url, ...(options?.invalidate ?? [])];
const keys = options?.invalidate ?? [];

if (!options?.skipUrlInvalidate) {
keys.push(url);
}

await this._cache.invalidate(...keys);

return result;
Expand Down
5 changes: 5 additions & 0 deletions cmd/serve/front/src/lib/fetcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export type MutateOptions = Pick<FetchOptions, 'fetch'> & {
* url but you can specify other urls to invalidate here.
*/
invalidate?: string[];
/**
* By default the url at which the mutate is done is invalidated but you
* can override this behavior by setting this flag to true.
*/
skipUrlInvalidate?: boolean;
};

export type QueryResult<T> = {
Expand Down
39 changes: 30 additions & 9 deletions cmd/serve/front/src/lib/localization/en.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Locale, Translations } from '$lib/localization';
import routes from '$lib/path';

const translations = {
// Authentication
Expand All @@ -10,8 +11,7 @@ const translations = {
'You need at least one target to deploy your application. Head to the <a href="/targets">create target</a> page to create one.',
'app.not_found': "Looks like the application you're looking for does not exist. Head back to the",
'app.not_found.cta': 'homepage',
'app.blankslate.title': 'Looks like you have no application yet. Start by',
'app.blankslate.cta': 'creating one!',
'app.blankslate': `Looks like you have no application yet. <br />Applications represents <strong>services you want to deploy</strong> on your infrastructure. Start by <a href="${routes.createApp}">creating one!</a>`,
'app.new': 'New application',
'app.edit': 'Edit application',
'app.delete': 'Delete application',
Expand All @@ -38,9 +38,7 @@ This action is IRREVERSIBLE and will DELETE ALL DATA associated with this applic
'app.vcs.help':
'If not under version control, you will still be able to manually deploy your application.',
'app.vcs.token': 'Access token',
'app.vcs.token.help.instructions':
'Token used to fetch the provided repository. Generally known as <strong>Personal Access Token</strong>, you can find some instructions for',
'app.vcs.token.help.leave_empty': ', leave empty if the repository is public.',
'app.vcs.token.help': `Token used to fetch the provided repository. Generally known as <strong>Personal Access Token</strong>, you can find some instructions for <a href="https://docs.github.com/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token" rel="noopener noreferrer" target="_blank">Github</a> and <a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html" rel="noopener noreferrer" target="_blank">Gitlab</a>, leave empty if the repository is public.`,
'app.environment.production': 'Production settings',
'app.environment.staging': 'Staging settings',
'app.environment.target': 'Deploy target',
Expand Down Expand Up @@ -76,8 +74,7 @@ This action is IRREVERSIBLE and will DELETE ALL DATA associated with this applic
'target.configuring': 'Target configuration in progress',
'target.configuring.description':
'Needed infrastructure is being deployed on the target, please wait.',
'target.blankslate.title': 'Looks like you have no target yet. Start by',
'target.blankslate.cta': 'creating one!',
'target.blankslate': `Looks like you have no target yet. <br />Targets determine on which host your <strong>applications will be deployed</strong> and which <strong>provider</strong> should be used. Start by <a href="${routes.createTarget}">creating one!</a>`,
'target.general': 'General settings',
'target.name.help': 'The name is being used only for display, it can be anything you want.',
'target.url.help':
Expand Down Expand Up @@ -127,6 +124,25 @@ You may reconsider and try to make the target reachable before deleting it.`,
'deployment.command.delete_target': 'Target removal',
'deployment.command.configure_target': 'Target configuration',
'deployment.command.deploy': 'Application deployment',
// Registries
'registry.new': 'New registry',
'registry.blankslate': `Looks like you have no custom registry yet. <br />If some of your images are <strong>hosted on private registries</strong>, <a href="${routes.createRegistry}">configure them here</a> to make them available.`,
'registry.not_found':
"Looks like the registry you're looking for does not exist. Head back to the",
'registry.not_found.cta': 'registries page',
'registry.delete': 'Delete registry',
'registry.delete.confirm': (name: string) =>
`Are you sure you want to delete the registry ${name}?`,
'registry.delete.failed': 'Deletion failed',
'registry.general': 'General settings',
'registry.url.help':
'Url of the registry. For a private Docker Hub registry, use the url <code>https://index.docker.io/v1/</code>.',
'registry.authentication': 'Authentication',
'registry.auth': 'Need authentication',
'registry.auth.help': 'Does the registry require authentication?',
'registry.username': 'Username',
'registry.password': 'Password',
'registry.name.help': 'The name is being used only for display, it can be anything you want.',
// Account
'profile.my': 'my profile',
'profile.logout': 'log out',
Expand Down Expand Up @@ -155,8 +171,10 @@ You may reconsider and try to make the target reachable before deleting it.`,
'deployment.promote.confirm': (number: number) =>
`The deployment #${number} will be promoted to the production environment. Do you confirm this action?`,
'deployment.promote.failed': 'Promote failed',
'deployment.blankslate.title': 'No deployment to show. Go ahead and',
'deployment.blankslate.cta': 'create the first one!',
'deployment.blankslate': (app: string) =>
`No deployment to show. Go ahead and <a href="${routes.createDeployment(
app
)}">create the first one!</a>`,
'deployment.environment': 'Environment',
'deployment.payload': 'Payload',
'deployment.payload.copy_curl': 'Copy cURL command',
Expand Down Expand Up @@ -204,6 +222,9 @@ You may reconsider and try to make the target reachable before deleting it.`,
'breadcrumb.target.new': 'New target',
'breadcrumb.target.settings': (name: string) => `${name} settings`,
'breadcrumb.jobs': 'Jobs',
'breadcrumb.registries': 'Registries',
'breadcrumb.registry.new': 'New registry',
'breadcrumb.registry.settings': (name: string) => `${name} settings`,
'breadcrumb.profile': 'Profile',
'breadcrumb.not_found': 'Not found',
// Footer
Expand Down
39 changes: 30 additions & 9 deletions cmd/serve/front/src/lib/localization/fr.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AppTranslations, Locale } from '$lib/localization';
import routes from '$lib/path';

export default {
code: 'fr',
Expand All @@ -14,8 +15,7 @@ export default {
'app.not_found':
"Il semblerait que l'application que vous recherchez n'existe pas. Retournez à la",
'app.not_found.cta': "page d'accueil",
'app.blankslate.title': 'Aucune application trouvée, commencez par',
'app.blankslate.cta': 'en créer une !',
'app.blankslate': `Aucune application pour le moment. <br />Les applications représentent les <strong>services que vous souhaitez déployer</strong> sur votre infrastructure. Commencez par <a href="${routes.createApp}">en créer une !</a>`,
'app.new': 'Nouvelle application',
'app.edit': "Modifier l'application",
'app.delete': "Supprimer l'application",
Expand All @@ -42,9 +42,7 @@ Cette action est IRRÉVERSIBLE et supprimera TOUTES LES DONNÉES associées sur
'app.vcs.help':
"Si vous n'utilisez pas de contrôle de version, vous pourrez toujours déployer manuellement votre application.",
'app.vcs.token': "Jeton d'accès",
'app.vcs.token.help.instructions':
"Jeton utilisé pour vous authentifier auprès du dépôt. Généralement connu sous le nom de <strong>Jeton d'accès personnel</strong>, vous pouvez trouver des instructions pour",
'app.vcs.token.help.leave_empty': ', laissez vide si le dépôt est public.',
'app.vcs.token.help': `Jeton utilisé pour vous authentifier auprès du dépôt. Généralement connu sous le nom de <strong>Jeton d'accès personnel</strong>, vous pouvez trouver des instructions pour <a href="https://docs.github.com/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token" rel="noopener noreferrer" target="_blank">Github</a> et <a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html" rel="noopener noreferrer" target="_blank">Gitlab</a>, laissez vide si le dépôt est public.`,
'app.environment.production': 'Paramètres de production',
'app.environment.staging': 'Paramètres de staging',
'app.environment.target': 'Cible de déploiement',
Expand Down Expand Up @@ -82,8 +80,7 @@ Cette action est IRRÉVERSIBLE et supprimera TOUTES LES DONNÉES associées sur
},
'target.configuring': 'Configuration de la cible en cours',
'target.configuring.description': `L'infrastructure nécessaire est en cours de déploiement, veuillez patienter.`,
'target.blankslate.title': 'Aucune cible trouvée, commencez par',
'target.blankslate.cta': 'en créer une !',
'target.blankslate': `Aucune cible pour le moment. <br />Les cibles déterminent sur quel hôte vos <strong>applications seront déployées</strong>. Commencez par <a href="${routes.createTarget}">en créer une !</a>`,
'target.general': 'Paramètres généraux',
'target.name.help': `Le nom est utilisé uniquement pour l'affichage. Vous pouvez choisir ce que vous voulez.`,
'target.url.help': `Toutes les applications déployées sur cette cible seront disponibles en tant que <strong>sous-domaine</strong> de cette URL racine (sans sous-chemin). Elle doit être <strong>unique</strong> parmi les cibles. Vous <strong>DEVEZ</strong> configurer un <strong>DNS wildcard</strong> pour les sous-domaines de telle sorte que <code>*.&lt;url configurée&gt;</code> redirige vers l'IP de cette cible.`,
Expand Down Expand Up @@ -130,6 +127,25 @@ Vous devriez probablement essayer de rendre la cible accessible avant de la supp
'deployment.command.delete_target': 'Suppression de la cible',
'deployment.command.configure_target': 'Configuration de la cible',
'deployment.command.deploy': "Déploiement de l'application",
// Registries
'registry.new': 'Nouveau registre',
'registry.blankslate': `Aucun registre pour le moment. <br />Si certaines de vos images sont hébergées sur <strong>des registres privés</strong>, <a href="${routes.createRegistry}">configurer les ici</a> de manière à les rendre disponibles.`,
'registry.not_found':
"Il semblerait que le registre que vous recherchez n'existe pas. Retournez à la",
'registry.not_found.cta': 'page des registres',
'registry.delete': 'Supprimer le registre',
'registry.delete.confirm': (name: string) =>
`Voulez-vous vraiment supprimer le registre ${name} ?`,
'registry.delete.failed': 'Erreur de suppression',
'registry.general': 'Paramètres généraux',
'registry.url.help':
"Url du registre. Pour un registre privé Docker Hub, utiliser l'url <code>https://index.docker.io/v1/</code>.",
'registry.authentication': 'Authentification',
'registry.auth': 'Authentification nécessaire',
'registry.auth.help': 'Le registre nécessite t-il une authentification ?',
'registry.username': "Nom d'utilisateur",
'registry.password': 'Mot de passe',
'registry.name.help': `Le nom est utilisé uniquement pour l'affichage. Vous pouvez choisir ce que vous voulez.`,
// Account
'profile.my': 'mon profil',
'profile.logout': 'se déconnecter',
Expand Down Expand Up @@ -159,8 +175,10 @@ Vous devriez probablement essayer de rendre la cible accessible avant de la supp
'deployment.promote.confirm': (number: number) =>
`Le déploiement #${number} sera promu sur l'environnement de production. Confirmez-vous cette action ?`,
'deployment.promote.failed': 'Erreur lors de la promotion',
'deployment.blankslate.title': 'Aucun déploiement trouvé. Commencez par',
'deployment.blankslate.cta': 'en créer un !',
'deployment.blankslate': (app: string) =>
`Aucun déploiement trouvé. Commencez par <a href="${routes.createDeployment(
app
)}">en créer un !</a>`,
'deployment.environment': 'Environnement',
'deployment.payload': 'Charge utile',
'deployment.payload.copy_curl': 'Copier la commande cURL',
Expand Down Expand Up @@ -209,6 +227,9 @@ Vous devriez probablement essayer de rendre la cible accessible avant de la supp
'breadcrumb.target.new': 'Nouvelle cible',
'breadcrumb.target.settings': (name: string) => `Paramètres de ${name}`,
'breadcrumb.jobs': 'Tâches',
'breadcrumb.registries': 'Registres',
'breadcrumb.registry.new': 'Nouveau registre',
'breadcrumb.registry.settings': (name: string) => `Paramètes de ${name}`,
'breadcrumb.profile': 'Profil',
'breadcrumb.not_found': 'Ressource introuvable',
// Footer
Expand Down
5 changes: 4 additions & 1 deletion cmd/serve/front/src/lib/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ const routes = {
targets: '/targets',
createTarget: '/targets/new',
editTarget: (id: string) => `/targets/${id}/edit`,
jobs: '/jobs'
jobs: '/jobs',
registries: '/registries',
createRegistry: '/registries/new',
editRegistry: (id: string) => `/registries/${id}/edit`
} as const;

export default routes;
84 changes: 84 additions & 0 deletions cmd/serve/front/src/lib/resources/registries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { POLLING_INTERVAL_MS } from '$lib/config';
import fetcher, { type FetchOptions, type FetchService, type QueryResult } from '$lib/fetcher';
import type { ByUserData } from '$lib/resources/users';

export type Registry = {
id: string;
name: string;
url: string;
credentials?: Credentials;
created_at: string;
created_by: ByUserData;
};

export type Credentials = {
username: string;
password: string;
};

export type CreateRegistry = {
name: string;
url: string;
credentials?: Credentials;
};

export type UpdateRegistry = {
name?: string;
url?: string;
credentials: Patch<{
username: string;
password?: string;
}>;
};

export interface RegistriesService {
create(payload: CreateRegistry): Promise<Registry>;
update(id: string, payload: UpdateRegistry): Promise<Registry>;
delete(id: string): Promise<void>;
fetchAll(options?: FetchOptions): Promise<Registry[]>;
fetchById(id: string, options?: FetchOptions): Promise<Registry>;
queryAll(): QueryResult<Registry[]>;
}

type Options = {
pollingInterval: number;
};

export class RemoteRegistriesService implements RegistriesService {
constructor(private readonly _fetcher: FetchService, private readonly _options: Options) {}

create(payload: CreateRegistry): Promise<Registry> {
return this._fetcher.post('/api/v1/registries', payload);
}

update(id: string, payload: UpdateRegistry): Promise<Registry> {
return this._fetcher.patch(`/api/v1/registries/${id}`, payload);
}

delete(id: string): Promise<void> {
return this._fetcher.delete(`/api/v1/registries/${id}`, {
invalidate: ['/api/v1/registries'],
skipUrlInvalidate: true
});
}

queryAll(): QueryResult<Registry[]> {
return this._fetcher.query('/api/v1/registries', {
refreshInterval: this._options.pollingInterval
});
}

fetchAll(options?: FetchOptions): Promise<Registry[]> {
return this._fetcher.get('/api/v1/registries', options);
}

fetchById(id: string, options?: FetchOptions): Promise<Registry> {
return this._fetcher.get(`/api/v1/registries/${id}`, options);
}
}

const service: RegistriesService = new RemoteRegistriesService(fetcher, {
pollingInterval: POLLING_INTERVAL_MS
});

export default service;
Loading

0 comments on commit bdb2484

Please sign in to comment.