Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for building a container from context archive #671

Closed
wants to merge 1 commit into from
Closed

Add support for building a container from context archive #671

wants to merge 1 commit into from

Conversation

LNSD
Copy link
Contributor

@LNSD LNSD commented Nov 13, 2023

This PR adds support for building a container image from a tar archive stream (i.e., NodeJs.ReadableStream).

Note
This functionality is already existing in testcontainers-go: dynamic-build-context

This covers the case where one wants to programmatically create a dynamic Dockerfile and context (e.g., using a template engine) without the overhead of writing to disk.

const tar = require('tar-stream');

const tarStream = tar.pack();
tarStream.entry({ name: 'alpine.Dockerfile' }, 
  `
    FROM alpine:latest
    CMD ["sleep", "infinity"]
  `
);
tarStream.finalize();

const container = await GenericContainer
  .fromContextArchive(tarStream, 'alpine.Dockerfile')
  .build();

Copy link

netlify bot commented Nov 13, 2023

Deploy Preview for testcontainers-node ready!

Name Link
🔨 Latest commit 2382dba
🔍 Latest deploy log https://app.netlify.com/sites/testcontainers-node/deploys/655e3bb5d063ee00080f24d2
😎 Deploy Preview https://deploy-preview-671--testcontainers-node.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

"@types/tmp": "^0.2.5"
"@types/tar-stream": "^3.1.3",
"@types/tmp": "^0.2.5",
"tar-stream": "^3.1.6"
Copy link
Contributor

Choose a reason for hiding this comment

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

tar-stream shouldn't be in dependencies 🤔 ?

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 only used it in the tests to generate a test tar stream. It is not used in the package's production code.

private readonly dockerfileName: string,
private readonly uuid: Uuid = new RandomUuid()
) {}

public withBuildArgs(buildArgs: BuildArgs): GenericContainerBuilder {
public withBuildArgs(buildArgs: BuildArgs): this {
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks to do the return this pattern consistent here 👏

@@ -103,3 +105,27 @@ describe("GenericContainer Dockerfile", () => {
await startedContainer.stop();
});
});

describe("GenericContainer fromContextArchive", () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks to add tests 🎉

import { getAuthConfig } from "../../auth/get-auth-config";
import { ImageName } from "../../image-name";
import { ImageClient } from "./image-client";
import AsyncLock from "async-lock";
import { log, buildLog, pullLog } from "../../../common";
import stream from "stream";
Copy link
Contributor

Choose a reason for hiding this comment

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

this is the node stream api https://nodejs.org/api/stream.html#stream right?
sometimes when I dont read it as

const stream = require('node:stream'); 

has this doubt 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is. I can change the import name with node:stream, but I don't know the difference.

@@ -105,3 +138,17 @@ export class GenericContainerBuilder {
.reduce((prev, next) => ({ ...prev, ...next }), {} as RegistryConfig);
}
}

async function newDockerignoreFilter(context: string): Promise<(path: string) => boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Only to curiosity in your approach, why newDockerignoreFilter function is not part from class GenericContainerBuilder as a private method take into account that you are only using it to this class responsabilities?

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 believe class methods should access or modify the object instance.

In this case, newDockerignoreFilter does not access nor modify any object fields. It can be seen as a factory method. It is a function that, given a context path, returns a .dockerignore based filter function.

@javierlopezdeancos
Copy link
Contributor

javierlopezdeancos commented Nov 14, 2023

After reviewing the PR I think that for me it is LGTM and approved.

@LNSD Thanks for the explanations about how the docker and dockerode api works to build an image with a dockerfile (recipe) and a files source (ingredients aka context), either through a path on our disk or through an external data source, In both cases, we have to package these files into a tar file and create a stream from it which is as the docker api requires in its context.

The feature itself is very powerful, being able to programmatically create a dynamic dockerfile and feed the stream with ingredients that do not exactly belong to our disk, a use case, an AWS stream.

I see the refactor that you include to allow the feature appropriate, promoting the creation of the source tar file which we will create the stream from.

Only there are some questions that I put in my review to give you my approval

@@ -44,6 +48,12 @@ export class GenericContainerBuilder {
return this;
}

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

I really really really really appreciate these comments for our methods and functions but, since they are not anywhere in the library, I don't know if it would be convenient to start doing it. 🤔

@LNSD

This comment was marked as resolved.

@cristianrgreco cristianrgreco added enhancement New feature or request minor Backward compatible functionality labels Nov 23, 2023
try {
log.debug(`Building image "${opts.t}" with context "${context}"...`);
Copy link
Collaborator

@cristianrgreco cristianrgreco Nov 23, 2023

Choose a reason for hiding this comment

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

What's the reasoning behind moving this logic to the GenericContainerBuilder? I liked the idea that the DockerImageClient can be used standalone and supports .dockerignore files out of the box. After this change this is no longer the case

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 understand your point.

I decided to raise the filtering to a higher abstraction level, recognizing that the .dockerignore filtering is a specific instance of passing a tar stream to the container runtime client. This approach streamlines the code and renders the DockerImageClient agnostic to the underlying filesystem, enhancing its versatility.

At the time of implementation, I faced two options:

  1. Introduce a method in GenericContainerBuilder and DockerImageClient for passing the stream directly to the container runtime client without .dockerignore filtering. This would create an additional flow in both classes.

  2. Maintain the DockerImageClient agnostic to the filesystem (not utilizing tar-fs) and keep the API straightforward by passing a stream to the container runtime client. Since the filtered stream is a subset of the passed stream, this approach avoids needing two separate APIs in both the client and the GenericContainerBuilder class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request minor Backward compatible functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants