Private Docker Registry Single Image API (Part 4)

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.

        
          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!

Posted by Adam Berkowitz