Skip to content

Commit

Permalink
VERYYYYYY early lando4 service and app build stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
pirog committed Apr 22, 2024
1 parent 4aa6d00 commit 1efb665
Show file tree
Hide file tree
Showing 9 changed files with 418 additions and 151 deletions.
408 changes: 285 additions & 123 deletions builders/lando-v4.js

Large diffs are not rendered by default.

68 changes: 51 additions & 17 deletions components/docker-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,18 +432,39 @@ class DockerEngine extends Dockerode {
stderro = '',
} = {}) {
const awaitHandler = async () => {
// stdin helpers
const resizer = container => {
const dimensions = {h: process.stdout.rows, w: process.stderr.columns};
if (dimensions.h != 0 && dimensions.w != 0) container.resize(dimensions, () => {});
};
const closer = (isRaw = process.isRaw) => {
process.stdout.removeListener('resize', resizer);
process.stdin.removeAllListeners();
process.stdin.setRawMode(isRaw);
process.stdin.resume();
};

return new Promise((resolve, reject) => {
let prevkey;
const CTRL_P = '\u0010';
const CTRL_Q = '\u0011';

const aopts = {stream: true, stdout: true, stderr: true, hijack: interactive, stdin: interactive};
const isRaw = process.isRaw;
const stdout = new PassThrough();
const stderr = new PassThrough();

runner.on('container', container => {
runner.on('stream', stream => {
const stdout = new PassThrough();
const stderr = new PassThrough();
container.attach(aopts, (error, stream) => {
if (error) runner.emit('error', error);

// handle attach dynamics
if (attach) {
// if tty and just pipe everthing to stdout
if (copts.Tty) stream.pipe(process.stdout);
if (copts.Tty) {
stream.pipe(process.stdout);
// otherwise we should be able to pipe both
else {
} else {
stdout.pipe(process.stdout);
stderr.pipe(process.stderr);
}
Expand All @@ -458,6 +479,18 @@ class DockerEngine extends Dockerode {
container.modem.demuxStream(stream, stdout, stderr);
}

// handle interactive
if (interactive) {
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.setRawMode(true);
process.stdin.pipe(stream);
process.stdin.on('data', key => {
if (prevkey === CTRL_P && key === CTRL_Q) closer(stream, isRaw);
prevkey = key;
});
}

// make sure we close child streams when the parent is done
stream.on('end', () => {
try {
Expand Down Expand Up @@ -485,32 +518,29 @@ class DockerEngine extends Dockerode {
allo += String(buffer);
if (!attach) debug.extend('stderr')(String(buffer));
});

runner.on('data', data => {
// emit error first
if (data.StatusCode !== 0) runner.emit('error', data);
// fire done no matter what?
runner.emit('done', data);
runner.emit('finished', data);
runner.emit('success', data);
});
});

// handle resolve/reject
runner.on('done', data => {
// @TODO: what about data?
closer(stream, isRaw);
resolve(makeSuccess(merge({}, data, {command: 'dockerode run', all: allo, stdout: stdouto, stderr: stderro}, {args: command})));
});
runner.on('error', error => {
closer(stream, isRaw);
reject(makeError(merge({}, args, {command: 'dockerode run', all: allo, stdout: stdouto, stderr: stderro}, {args: command}, {error})));
});
});
};

// handles the callback to super.run
// we basically need this just to handle dockerode modem errors
const callbackHandler = error => {
const callbackHandler = (error, data) => {
// emit error first
if (error) runner.emit('error', error);
if (data.StatusCode !== 0) runner.emit('error', data);
// fire done no matter what?
runner.emit('done', data);
runner.emit('finished', data);
runner.emit('success', data);
};

// error if no command
Expand All @@ -521,10 +551,14 @@ class DockerEngine extends Dockerode {
// some good default createOpts
const defaultCreateOptions = {
AttachStdin: interactive,
AttachStdout: attach,
AttachStderr: attach,
HostConfig: {AutoRemove: true},
Tty: false || interactive || attach,
OpenStdin: true,
StdinOnce: true,
};

// merge our create options over the defaults
const copts = merge({}, defaultCreateOptions, createOptions);
// collect some args we can merge into promise resolution
Expand Down
11 changes: 8 additions & 3 deletions components/l337-v4.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const hasInstructions = require('../utils/has-instructions');
class L337ServiceV4 extends EventEmitter {
#data

static debug = require('debug')('l337-service-v4');
static debug = require('debug')('@lando/l337-service-v4');
static bengineConfig = {};
static builder = require('../utils/get-docker-x')();
static orchestrator = require('../utils/get-compose-x')();
Expand Down Expand Up @@ -73,6 +73,10 @@ class L337ServiceV4 extends EventEmitter {
return this.#data.states;
}

get _data() {
return this.#data;
}

constructor(id, {
appRoot = path.join(os.tmpdir(), nanoid(), id),
buildArgs = {},
Expand Down Expand Up @@ -370,8 +374,9 @@ class L337ServiceV4 extends EventEmitter {
volume.source = path.join(this.appRoot, volume.source);
}

// if target exists then bind otherwise vol
volume.type = fs.existsSync(volume.source) ? 'bind' : 'volume';
// we make an "exception" for any /run/host-services things that are in the docker vm
if (volume.source.startsWith('/run/host-services')) volume.type = 'bind';
else volume.type = fs.existsSync(volume.source) ? 'bind' : 'volume';

// return
return volume;
Expand Down
13 changes: 9 additions & 4 deletions hooks/app-add-v4-services.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,23 @@ module.exports = async (app, lando) => {

// instantiate each service
_.forEach(app.v4.parsedConfig, config => {
// Throw a warning if service is not supported
if (_.isEmpty(_.find(lando.factory.get(), {api: 4, name: config.type}))) {
app.log.warn('%s is not a supported v4 service type.', config.type);
// Throw a warning if builder is not supported
if (_.isEmpty(_.find(lando.factory.get(), {api: 4, name: config.builder}))) {
app.log.warn('%s is not a supported v4 builder.', config.builder);
}

// @TODO:
// if we have routing information lets pass that through
// in v3 "type" was parsed into a builder and a version but in v4 we use a more generic "router"
// concept that lets the "entrypoint builder"

// get any cached info so we can set that as a base in the service
const info = _(_.find(app.v4.cachedInfo, {service: config.name, api: 4}))
.pick(['healthy', 'image', 'state', 'tag'])
.value();

// retrieve the correct class and mimic-ish v4 patterns to ensure faster loads
const Service = lando.factory.get(config.type, config.api);
const Service = lando.factory.get(config.builder, config.api);
Service.bengineConfig = lando.config.engineConfig;
Service.builder = lando.config.dockerBin;
Service.orchestrator = lando.config.orchestratorBin;
Expand Down
34 changes: 32 additions & 2 deletions hooks/app-run-v4-build-steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = async (app, lando) => {
if (_.isEmpty(data)) {
lando.cache.remove(app.v4.preLockfile);
lando.cache.remove(app.v4.postLockfile);
app.log.debug('removed v4 build locks');
app.log.debug('removed v4 image build locks');
}
});
});
Expand All @@ -31,7 +31,7 @@ module.exports = async (app, lando) => {
.filter(service => _.includes(buildV4Services, service.id))
.value();

app.log.debug('going to build v4 services', services.map(service => service.id));
app.log.debug('going to build v4 images', services.map(service => service.id));

// now build an array of promises with our services
const tasks = services.map(service => {
Expand Down Expand Up @@ -91,6 +91,7 @@ module.exports = async (app, lando) => {
const dir = service?.error?.context?.context ?? os.tmpdir();
service.error.logfile = path.join(dir, `error-${nanoid()}.log`);
data.image = 'busybox';
data.user = 'root';
data.command = require('../utils/get-v4-image-build-error-command')(service.error);
data.volumes = [`${service.error.logfile}:/tmp/error.log`];
}
Expand All @@ -109,4 +110,33 @@ module.exports = async (app, lando) => {
// and reset the compose cache as well
app.v4.updateComposeCache();
});

// run app build steps
app.events.on('pre-start', 110, async () => {
// get buildable services
const buildV4Services = _(app.v4.parsedConfig)
.filter(service => _.includes(_.get(app, 'opts.services', app.services), service.name))
.map(service => service.name)
.value();

// filter out any services that dont need to be built
const services = _(app.v4.services)
.filter(service => _.includes(buildV4Services, service.id))
.filter(service => typeof service.buildApp === 'function')
.filter(service => service?.info?.state?.IMAGE === 'BUILT')
.filter(service => service?.info?.state?.APP !== 'BUILT')
.value();

// and then run them in parallel
await Promise.all(services.map(async service => {
try {
await service.buildApp();
} catch (error) {
app.addMessage(require('../messages/app-build-v4-error')(error), error, true);
}
}));

// and reset the compose cache as well
app.v4.updateComposeCache();
});
};
2 changes: 1 addition & 1 deletion lib/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ module.exports = class Factory {

// if service builder is a function and not a class then we need to pass the parent in until we get resolution
if (_.isFunction(service.builder) && !isClass(service.builder)) {
service.builder = service.builder(this.get(service.parent, service.api), service.config);
service.builder = service.builder(this.get(service.parent, service.api), service.defaults ?? service.config);
}

// if we are here then we *should* have the class we need?
Expand Down
15 changes: 15 additions & 0 deletions messages/app-build-v4-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

const {bold} = require('color');

// checks to see if a setting is disabled
module.exports = error => ({
title: `Could not build app in "${error.id}!"`,
type: 'warn',
detail: [
`App build steps failed in ${bold(error.context.path)}`,
`Rerun with ${bold('lando rebuild --debug')} to see the entire build log and look for errors.`,
`When you've resolved the build issues you can then:`,
],
command: 'lando rebuild',
});
14 changes: 14 additions & 0 deletions utils/generate-build-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable max-len */
'use strict';

module.exports = (contents, user, mount = '/app') => `
#!/bin/sh
# clean up and setup ssh-auth
rm -rf /run/ssh-${user}.sock
sudo socat UNIX-LISTEN:/run/ssh-${user}.sock,fork,user=pirog,group=20,mode=777 UNIX-CONNECT:/run/host-services/ssh-auth.sock &
# temp stuff for demo purposes
git config --global --add safe.directory ${mount}
${contents}
`;
4 changes: 3 additions & 1 deletion utils/parse-v4-services.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ module.exports = services => _(services)
.map((service, name) => _.merge({}, {
name,
api: require('./get-service-api-version')(service.api),
builder: service.type.split(':')[0] ?? 'lando',
config: _.omit(service, ['api', 'meUser', 'moreHttpPorts', 'primary', 'scanner', 'sport', 'type']),
legacy: {
meUser: service.meUser ?? 'www-data',
moreHttpPorts: service.moreHttpPorts ?? [],
sport: service.sport ?? '443',
},
primary: service.primary ?? false,
router: service.type.split(':')[1],
scanner: service.scanner ?? false,
type: service.type ?? 'lando',
type: service.type,
}))
.value();

0 comments on commit 1efb665

Please sign in to comment.