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

Initial proof-of-concept code for open source build module #1

Closed
wants to merge 6 commits into from

Conversation

CameronDiver
Copy link
Contributor

@CameronDiver CameronDiver commented Jan 25, 2017

The following code is meant as a proof of concept to supplement the spec that is currently in review. Any issues that come up in this code review will be reflected in the spec (and vice versa), and then once the spec is approved we can move forward, with the further milestones and features.

Currently we have a simple build function which handles the streaming of data to/from the docker daemon, a simple plugin based architecture which contains the bare minimum to get a build to complete, and an example program using the architecture.

The example program resolves Dockerfile.template files (by assuming nuc etc for %%RESIN_...%% macros) midstream, using the hooks provided, and proceeds to build the project.

Simple package.json and tsconfig.json files have been created, but given my unfamilarity with typescript they're probably not correct, @Page- I could do with your input on these.

Cameron Diver added 2 commits January 25, 2017 14:02
* Added Apache-2.0 license
* Added .gitignore
* Added tsconfig.json

Signed-off-by: Cameron Diver <cameron@resin.io>
* Create plugin interface
* Created Builder class to interface with docker daemon and call plugins
* Create example application which resolves a Dockerfile.template
project and streams it to docker to be built
* Add utils module with docker info extraction functions

Signed-off-by: Cameron Diver <cameron@resin.io>
@CameronDiver
Copy link
Contributor Author

Also link to spec: https://github.com/resin-io/hq/pull/567

Copy link

@pimterry pimterry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know much about how the builder works, so I'll leave comments on the details of that to others (although do shout if it'd be useful for me to jump in there too). I've sprinkled a few TypeScript comments around though.

It looks like tsconfig.json is already on master, but I can't see package.json anywhere? That should definitely come in 😄. A few comments on tsconfig:

  • I think we're settling on noImplicitAny (which will fail the compile if it ever can't accurately infer a value) and strictNullChecks (which forces you to to declare which variables can be null, and check nullable values if you want to them to go where they might explode) as standard settings to enable.
  • You haven't committed the build output here. I'm not sure what the 'right' solution is - the CS projects all seem to have build output included, some newer projects seem less sure - but it'd be nice to explicitly settle on whichever option we're going for and do it everywhere.
  • You should specify a target. It defaults to es3, which is probably way below what you want. Letting typescript use modern features will make the resulting code a slightly faster and smaller and simpler (it won't have to include workarounds), and there's also quite a few language features it will refuse to compile if you don't specify a target that supports them. es5 is probably reasonable.
  • You're already outputting declarations, but it'd be nice to do sourcemaps too for easier debugging (just set sourceMap: true), and to reference the declarations in types in your package.json, if you haven't already.

@@ -0,0 +1,83 @@
const tar = require('tar-stream');
const fs = require('fs');
const _ = require('lodash');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should try and use real imports where we can (i.e. import fs = require('fs'), not const) as they'll let use real type definitions, if we can get them. You can mostly pull type definitions from npm, e.g. npm install --save-dev @types/lodash. fs is part of node (@types/node), doesn't look like tar-stream has definitions, so that one could stay as-is (unless you're feeling super keen).

* @returns {ReadableStream}
* A stream which when read, produces the project tar archive
*/
let getProjectStream = (inputStream : NodeJS.ReadableStream) : NodeJS.ReadableStream => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: function definitions should probably be const.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hadn't considered that actually, good spot.

// obviously this will depend on the project type, so just support Dockerfile.template for POC
if(header.name === './Dockerfile.template') {
//this.templateContent = stream.read(header.size).toString();
//console.log(this.templateContent);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ Commented out code should probably go

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely :p

src/index.ts Outdated
const JSONStream = require('JSONStream');
const duplexify = require('duplexify');
const es = require('event-stream');
const _ = require('lodash');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ Quite a few of these (fs, path, lodash, event-stream) could easily have proper types too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial thinking was to only require "properly" when a type is used, but given our discussion on FD and procbots, and also for arg types which hadn't really crossed my mind, I'll be changing all of these to import properly.

Copy link
Contributor

@dfunckt dfunckt Jan 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A practice I like from my Python days is to alphabetically sort imports, and also group them by "distance" -- i.e. platform imports first, external deps second, project locals last. I know this isn't practiced in JS in general (or Resin), but you might want to consider.

src/index.ts Outdated

Promise.promisifyAll(Docker.prototype);
Promise.promisifyAll(fs);
Promise.promisifyAll(tar);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know about tar and Docker, but mz might be of interest if you're looking for promisified builtins like fs (and has published and promisified type definitions too).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I'll look into that.

src/index.ts Outdated
pack.pipe(stream);
// ...and return it for reading
resolve(stream);
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation here is a little confusing - looks like the then is inside the all, but it's not.

let extract = tar.extract();
let pack = tar.pack();

let proj : ProjectType = new ProjectType();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript can infer the type of the variable automatically, so the var need not be explicitly typed here:

let proj = new ProjectType();

It can probably also be const as the reference itself is immutable.

*/
let getProjectStream = (inputStream : NodeJS.ReadableStream) : NodeJS.ReadableStream => {
let extract = tar.extract();
let pack = tar.pack();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two can also be const. IMO it's best to always think and type const by default and only change to let if necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fully agree.

extract.on('entry', (header, stream, next) => {
// If the project type handler does not require the file,
// add it to the archive unchanged
if(!proj.provideEntry(stream, header)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space.

// Send the old tar archive to be unpacked
inputStream.pipe(extract);


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra new line.

}
};

let builder = new Builder('/var/run/docker.sock');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The socket should probably be configurable instead of hardcoded here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the example implementation, so in this case hardcoded is fine. This file is an example of the lib being used, in which case the socket path would be set.


private templateContent : string = "";

public provideEntry(stream, header) : boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make sense to specify argument types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would, I just havn't gotten round to making the example code nice as it's more of a test for now. Really this PR so far is to catch the oddities of TS going forward.

src/index.ts Outdated
*/
export default class Builder {

private docker;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the type stuff, (in addition to your comment above) is not fully implemented, both on our side and the modules which we are using. The main thing to get past at the moment is what to do when type information doesn't exist.

* Included build output in lib/
* Conform to initial tslint settings - Will change
* Use const where possible
* Try to use correct imports all of the time
* Use mz/fs instead of fs for promisified file system functions

Signed-off-by: Cameron Diver <cameron@resin.io>
Signed-off-by: Cameron Diver <cameron@resin.io>
src/utils.ts Outdated
function extractArrowMessage(message: string) : string | undefined {
let arrowTest = /^\s*-+>\s*(.+)/i;
let match;
if (match = arrowTest.exec(message))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep braces even for single line ifs - it's far too easy to add an extra line and forget you suddenly have to add braces

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine by me, @hedss we should add this to the tslint spec.

src/utils.ts Outdated
if (match = arrowTest.exec(message))
return match[1];
else
return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally would go for dropping semi-colons

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean dropping semi-colons everywhere possible, or just for returns for example? It would be nice to formalize this, for my own and the tslint specs sake.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd go for dropping them from everywhere, unless explicitly required (which is almost never, and the cases I know of where they're required are a little questionable) as they just add some visual noise. I think TS would also catch the rare case due to types not matching up

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, thanks!

src/utils.ts Outdated
if (match = arrowTest.exec(message))
return match[1];
else
return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this able to be just return?

src/index.ts Outdated
// Add this file to the tar archive
// FIXME: Use streams to add to the tar archive
return pack.entryAsync({name: file, size: stats.size}, fs.readFileSync(relPath));
}))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put the final ) on a new line, so that it lines up with the indentation level it starts with - I find it makes it much easier to match up starting/opening brackets and the indented blocks they contain

src/index.ts Outdated

// Add this file to the tar archive
// FIXME: Use streams to add to the tar archive
return pack.entryAsync({name: file, size: stats.size}, fs.readFileSync(relPath));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get rid of the synchronous fs accesses pleases

src/index.ts Outdated
})
.catch((err: Error) => {
// Call the plugin's error handler
instance.callHook('buildFailure', [err.toString()]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like it would make sense to pass the error object itself, that way the hook would have a lot more control over how to handle it (especially as matching strings often falls down if you're working in different languages)

package.json Outdated
"@types/lodash": "^4.14.51",
"@types/mz": "0.0.30",
"@types/node": "^7.0.2",
"@types/promise": "^7.1.30"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There looks to be imports for 3 different types of promises, do we need them all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah no, you're correct. We only need the es6 and bluebird promises.

src/index.ts Outdated
const duplexify = require('duplexify');
// Following types are available, but do not work...
const Docker = require('dockerode');
const es = require('event-stream');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be an import, I see the types for it are in package.json?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I can't get them to work quite right. Myself, @pimterry and @hedss have been discussing the best way to fix this. The dockerode import at least can be fixed with DefinitelyTyped/DefinitelyTyped#14187

src/index.ts Outdated
// Following types are available, but do not work...
const Docker = require('dockerode');
const es = require('event-stream');
const JSONStream = require('JSONStream');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be an import, I see the types for it are in package.json?

package.json Outdated
"name": "resin-docker-build",
"version": "1.0.0",
"description": "A containerised builder which interacts with the docker remote API to perform builds.",
"main": "lib/index.js",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand it there should be an entry for the .ts types, so that people depending on this module can also benefit from typings

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

* Use async fs calls
* Remove unnecessary promise import
* formatting changes
* Use rest vars for callHooks, to avoid wrapping args in []
* Return error object instead of string

Signed-off-by: Cameron Diver <cameron@resin.io>
src/utils.ts Outdated
if(match = shaRegex.exec(extract)) {
export function extractLayer(message: string): string | undefined {
const extract = extractArrowMessage(message);
if(extract !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's meant to be a space before the paren here - we really need to get ts linting set up

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I wasn't aware that that was what we wanted (it's not in the coding standards). I've added it to the tslint now.

src/plugin.ts Outdated
* This hook will be called in the event of a build failure.
*
* @param {string} error
* This parameter will be populated with a string representation of the error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is out of date now, same with the jsdoc type above (is there any way to have it read the type from the typescript annotations? that'd be awesome)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little bit of research makes me think that it's not currently possible. At the very least it would be good if we had a tslint rule which would check the comments against the types in the code, which would catch things like this.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is TypeDoc: https://github.com/TypeStrong/typedoc.

It's pretty close to JSDoc, but you just drop the parameter types entirely, and it pulls them in from the TS compiler. Very minimally documented, not well known, and I suspect it's fairly immature. The CloudFlare team are using it though - https://blog.cloudflare.com/generating-documentation-for-typescript-projects/ - and their examples in that post make for one of the better intros around.

Signed-off-by: Cameron Diver <cameron@resin.io>
src/utils.ts Outdated
}

function extractArrowMessage(message: string): string | undefined {
let arrowTest = /^\s*-+>\s*(.+)/i
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can both be const

src/index.ts Outdated
private callHook = (hook: string, ...args: any[]) : any => {
if (hook in this.hooks) {
// Spread the arguments onto the callback function
let fn = this.hooks[hook]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be const

src/index.ts Outdated
* A stream which is connected to the output of the docker daemon
*/
public buildDir(dirPath: string, buildOpts: Object): Promise<NodeJS.ReadableStream> {
return new Promise<NodeJS.ReadableStream>((resolve, reject) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to manually construct a promise here, you chain just return the chain we have just below, check out http://bluebirdjs.com/docs/anti-patterns.html#the-explicit-construction-anti-pattern

src/index.ts Outdated

const outputStream = res
// parse the json objects
.pipe(JSONStream.parse())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to catch errors here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not be propagated to the catch block of the parent promise chain?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, this is a stream pipe rather than a promise chain - you need to add a separate .on 'error', (err) -> handler to each stream that can produce errors, if there isn't an error handler for a stream that produces an error then it will crash the nodejs process

src/index.ts Outdated
.pipe(JSONStream.parse())
// Don't use fat-arrow syntax here, to capture 'this' from es
.pipe(es.through(function(data: any) {
if (data.error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If data can be any, then this could throw if it's undefined | null

src/index.ts Outdated
* Example:
* new Builder('/var/run/docker.sock')
*/
constructor(dockerPath: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to have this take any valid dockerode options, or even a dockerode object directly? That way we automatically support building on remote hosts

src/index.ts Outdated
*/
export default class Builder {

private docker: any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any way to type this as a dockerode instance?

.gitignore Outdated
@@ -1,3 +1,2 @@
# Ignore javascript files
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth removing the comment too ;)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants