Introduction
In the last post of this series, I showed you how to set up an endpoint to get a list of all the docker images in your private registry. Now, I'll get into how to get details about those images.
Basically I wanted to get (to start) two things for each image
- a list of the available tags
- a README
Getting the tags is pretty straightforward (there's a registry API endpoint for them). It takes the format /v2/${group}/${name}/tags/list
. Using the group/name structure will be helpful in a moment to get readme files as well.
But, there's no API support for readmes or documentation of any kind for images. So, I made my own! It extends /api/images
to be /api/images/:group/:name
. For example, you could make requests to /api/images/hello/world
for information about the hello/world image.
This api structure also helps the architecture of our files. You can now create folders with documents on the server and reference them easily. For example I placed all of the readmes in /public/readmes
as markdown files. When a request is made to /api/images/hello/world
it will return the contents of /public/readmes/hello/world.md
. Let's set it up!
readme support
Dependencies
First things first, getting the contents of the readmes cleanly will require a few dependencies.
- fs (built into node)
- path (also built into node)
- showdown - bi-directional markdown/HTML converter
- jsdom - a set of web standards built in javascript
- dompurify - an html sanitizer
npm install --save-dev dompurify showdown jsdom
// server.js after the other requires
const createDOMPurify = require('dompurify');
const fs = require('fs');
const path = require('path');
const showdown = require('showdown');
const { JSDOM } = require('jsdom');
Safe(r) Markdown
It's important to make sure that when someone makes a request to this endpoint we're setting up, it's as secure as we can make it. The thing about markdown files is that (in theory) someone could add a malicious script to one and then execute the script on our user's browser. So I wanted a way to sanitize them on the server before the markup they contain is returned to the client. In the end, the function to do that is pretty straightforward. It will
- be asynchronous
- take in a path to a file
- read the file
- convert the markdown to HTML
- return a sanitized version
It looks like this...
/**
*
* Read a markdown file and return sanitized html.
*
* @param {string} path - a file path on the local system
*/
async function getHTML(path) {
// read the markdown file as a string
const file = fs.readFileSync(path, 'utf-8')
// convert the markdown to html
const dirty = converter.makeHtml(file);
// sanitize before output
return domPurify.sanitize(dirty);
}
Single Image Endpoint
With that little bit of setup taken care of, you can make a new endpoint under the previous one. It will use two query parameters in the request a :group
and a :name
. Like the endpoint for all the images it will involve an asynchronous callback and return a json response.
HTML from markdown
// endpoint for all images
app.get('/api/images', async (req, res) => {
// everything before...
})
// start building the endpoint for individual images
app.get('/api/images/:group/:name', async (req, res) => {
// separate out the group and name from the request
const { params: { group, name } } = req;
// get the path of the md file for the request.
const filePath = path.join(__dirname, `/public/readmes/${group}/${name}.md`);
// turn the file into an html string using the getHTML function
const html = await getHTML(filePath)
.then(response => response)
.catch(err => console.log(err))
// make an object with the html.
const data = { html }
// return json as a response
res.json(data);
});
Tags from the registry
I also want to make sure that the tags are included in response. As I mentioned above, there's an endpoint on the docker registry api for this. So, just like you can make a proxied request of a list of all the images, you can make one for just the tags. I want to await
this request as well so that everything stays within the asynchronous idea.
// endpoint for all images
app.get('/api/images', async (req, res) => {
// everything before...
})
// start building the endpoint for individual images
app.get('/api/images/:group/:name', async (req, res) => {
// separate out the group and name from the request
const { params: { group, name } } = req;
// get the path of the md file for the request.
const filePath = path.join(__dirname, `/public/readmes/${group}/${name}.md`);
// turn the file into an html string using the getHTML function
const html = await getHTML(filePath)
.then(response => response)
.catch(err => console.log(err))
// fetch the tags from the registry api
const tags = await axios.get(`http://registry/v2/${group}/${name}/tags/list`)
.then(response => response.data.tags )
.catch((err) => {
console.log('Axios error -> tags', err);
return err;
});
// make an object with the html and tags.
const data = { html, tags }
// return json as a response
res.json(data);
});
Conclusion
At this point, you can now build a front end that makes requests to these endpoints to get
- a list of all docker images
- tags for individual images
- readmes for individual images
Having a private docker registry is extremely useful. But when you have many people working with the images it contains, it's even better when you can get detailed information about all the images.
I hope you enjoyed this series!