mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-02-21 17:56:57 +00:00
Merge pull request #1890 from balena-io/1870-multi-dockerignore
Add --multi-dockerignore (-m) option to push/build/deploy commands
This commit is contained in:
commit
0ce035f379
213
doc/cli.markdown
213
doc/cli.markdown
@ -1943,11 +1943,11 @@ containers. The synchronization is only in one direction, from this machine to
|
|||||||
the device, and changes made on the device itself may be overwritten.
|
the device, and changes made on the device itself may be overwritten.
|
||||||
This feature requires a device running supervisor version v9.7.0 or greater.
|
This feature requires a device running supervisor version v9.7.0 or greater.
|
||||||
|
|
||||||
REGISTRY SECRETS
|
REGISTRY SECRETS
|
||||||
The --registry-secrets option specifies a JSON or YAML file containing private
|
The --registry-secrets option specifies a JSON or YAML file containing private
|
||||||
Docker registry usernames and passwords to be used when pulling base images.
|
Docker registry usernames and passwords to be used when pulling base images.
|
||||||
Sample registry-secrets YAML file:
|
Sample registry-secrets YAML file:
|
||||||
|
```
|
||||||
'my-registry-server.com:25000':
|
'my-registry-server.com:25000':
|
||||||
username: ann
|
username: ann
|
||||||
password: hunter2
|
password: hunter2
|
||||||
@ -1957,7 +1957,7 @@ Sample registry-secrets YAML file:
|
|||||||
'eu.gcr.io': # Google Container Registry
|
'eu.gcr.io': # Google Container Registry
|
||||||
username: '_json_key'
|
username: '_json_key'
|
||||||
password: '{escaped contents of the GCR keyfile.json file}'
|
password: '{escaped contents of the GCR keyfile.json file}'
|
||||||
|
```
|
||||||
For a sample project using registry secrets with the Google Container Registry,
|
For a sample project using registry secrets with the Google Container Registry,
|
||||||
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
||||||
|
|
||||||
@ -1965,37 +1965,54 @@ If the --registry-secrets option is not specified, and a secrets.yml or
|
|||||||
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
||||||
this file will be used instead.
|
this file will be used instead.
|
||||||
|
|
||||||
DOCKERIGNORE AND GITIGNORE FILES
|
DOCKERIGNORE AND GITIGNORE FILES
|
||||||
The balena CLI will use a '.dockerignore' file (if any) at the source directory
|
By default, the balena CLI will use a single ".dockerignore" file (if any) at
|
||||||
in order to decide which source files to exclude from the "build context" sent
|
the project root (--source directory) in order to decide which source files to
|
||||||
to balenaCloud, Docker or balenaEngine. In a microservices / multicontainer
|
exclude from the "build context" (tar stream) sent to balenaCloud, Docker daemon
|
||||||
application, the source directory is usually where the 'docker-compose.yml'
|
or balenaEngine. In a microservices (multicontainer) application, the source
|
||||||
file is located, and therefore the '.dockerignore' file should be located
|
directory is the directory that contains the "docker-compose.yml" file.
|
||||||
alongside the 'docker-compose.yml' file. Matching patterns may be prefixed with
|
|
||||||
the service's directory name (relative to the source directory) in order to
|
|
||||||
apply to that service only (e.g. 'service1/node_modules').
|
|
||||||
|
|
||||||
Previous balena CLI releases (before v12.0.0) also took '.gitignore' files
|
The --multi-dockerignore (-m) option may be used with microservices (multicontainer)
|
||||||
into account. This behavior is deprecated, but may still be enabled with the
|
applications that define a docker-compose.yml file. When this option is used,
|
||||||
--gitignore (-g) option if compatibility is required. This option will be
|
each service subdirectory (defined by the `build` or `build.context` service
|
||||||
removed in the CLI's next major version release (v13).
|
properties in the docker-compose.yml file) is filtered separately according to
|
||||||
|
a .dockerignore file defined in the service subdirectory. If no .dockerignore
|
||||||
|
file exists in a service subdirectory, then only the default .dockerignore
|
||||||
|
patterns (see below) apply for that service subdirectory.
|
||||||
|
|
||||||
When --gitignore (-g) is NOT provided (i.e. when not in v11 compatibility mode),
|
When the --multi-dockerignore (-m) option is used, the .dockerignore file (if
|
||||||
a few "hardcoded" dockerignore patterns are also used and "merged" (in memory)
|
any) defined at the overall project root will be used to filter files and
|
||||||
with the patterns found in the '.dockerignore' file (if any), in the following
|
subdirectories other than service subdirectories. It will not have any effect
|
||||||
order:
|
on service subdirectories, whether or not a service subdirectory defines its
|
||||||
|
own .dockerignore file. Multiple .dockerignore files are not merged or added
|
||||||
|
together, and cannot override or extend other files. This behavior maximises
|
||||||
|
compatibility with the standard docker-compose tool, while still allowing a
|
||||||
|
root .dockerignore file (at the overall project root) to filter files and
|
||||||
|
folders that are outside service subdirectories.
|
||||||
|
|
||||||
|
Balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||||
|
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||||
|
option if compatibility is required. This option is mutually exclusive with
|
||||||
|
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||||
|
release (v13).
|
||||||
|
|
||||||
|
Default .dockerignore patterns
|
||||||
|
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
|
||||||
|
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||||
|
patterns found in the applicable .dockerignore files, in the following order:
|
||||||
|
```
|
||||||
**/.git
|
**/.git
|
||||||
< user's patterns from the '.dockerignore' file, if any >
|
< user's patterns from the applicable '.dockerignore' file, if any >
|
||||||
!**/.balena
|
!**/.balena
|
||||||
!**/.resin
|
!**/.resin
|
||||||
!**/Dockerfile
|
!**/Dockerfile
|
||||||
!**/Dockerfile.*
|
!**/Dockerfile.*
|
||||||
!**/docker-compose.yml
|
!**/docker-compose.yml
|
||||||
|
```
|
||||||
If necessary, the effect of the '**/.git' pattern may be modified by adding
|
These patterns always apply, whether or not .dockerignore files exist in the
|
||||||
"counter patterns" to the '.dockerignore' file, for example '!service1/.git'.
|
project. If necessary, the effect of the `**/.git` pattern may be modified by
|
||||||
For documentation on pattern format, see:
|
adding counter patterns to the applicable .dockerignore file(s), for example
|
||||||
|
`!mysubmodule/.git`. For documentation on pattern format, see:
|
||||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||||
- https://www.npmjs.com/package/@balena/dockerignore
|
- https://www.npmjs.com/package/@balena/dockerignore
|
||||||
|
|
||||||
@ -2085,6 +2102,10 @@ No-op and deprecated since balena CLI v12.0.0
|
|||||||
|
|
||||||
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
||||||
|
|
||||||
|
#### --multi-dockerignore, -m
|
||||||
|
|
||||||
|
Have each service use its own .dockerignore file. See "balena help push".
|
||||||
|
|
||||||
#### --nogitignore, -G
|
#### --nogitignore, -G
|
||||||
|
|
||||||
No-op (default behavior) since balena CLI v12.0.0. See "balena help push".
|
No-op (default behavior) since balena CLI v12.0.0. See "balena help push".
|
||||||
@ -2157,11 +2178,11 @@ found, it will look for a Dockerfile[.template] file (or alternative Dockerfile
|
|||||||
specified with the `--dockerfile` option), and if no dockerfile is found, it
|
specified with the `--dockerfile` option), and if no dockerfile is found, it
|
||||||
will try to generate one.
|
will try to generate one.
|
||||||
|
|
||||||
REGISTRY SECRETS
|
REGISTRY SECRETS
|
||||||
The --registry-secrets option specifies a JSON or YAML file containing private
|
The --registry-secrets option specifies a JSON or YAML file containing private
|
||||||
Docker registry usernames and passwords to be used when pulling base images.
|
Docker registry usernames and passwords to be used when pulling base images.
|
||||||
Sample registry-secrets YAML file:
|
Sample registry-secrets YAML file:
|
||||||
|
```
|
||||||
'my-registry-server.com:25000':
|
'my-registry-server.com:25000':
|
||||||
username: ann
|
username: ann
|
||||||
password: hunter2
|
password: hunter2
|
||||||
@ -2171,7 +2192,7 @@ Sample registry-secrets YAML file:
|
|||||||
'eu.gcr.io': # Google Container Registry
|
'eu.gcr.io': # Google Container Registry
|
||||||
username: '_json_key'
|
username: '_json_key'
|
||||||
password: '{escaped contents of the GCR keyfile.json file}'
|
password: '{escaped contents of the GCR keyfile.json file}'
|
||||||
|
```
|
||||||
For a sample project using registry secrets with the Google Container Registry,
|
For a sample project using registry secrets with the Google Container Registry,
|
||||||
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
||||||
|
|
||||||
@ -2179,37 +2200,54 @@ If the --registry-secrets option is not specified, and a secrets.yml or
|
|||||||
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
||||||
this file will be used instead.
|
this file will be used instead.
|
||||||
|
|
||||||
DOCKERIGNORE AND GITIGNORE FILES
|
DOCKERIGNORE AND GITIGNORE FILES
|
||||||
The balena CLI will use a '.dockerignore' file (if any) at the source directory
|
By default, the balena CLI will use a single ".dockerignore" file (if any) at
|
||||||
in order to decide which source files to exclude from the "build context" sent
|
the project root (--source directory) in order to decide which source files to
|
||||||
to balenaCloud, Docker or balenaEngine. In a microservices / multicontainer
|
exclude from the "build context" (tar stream) sent to balenaCloud, Docker daemon
|
||||||
application, the source directory is usually where the 'docker-compose.yml'
|
or balenaEngine. In a microservices (multicontainer) application, the source
|
||||||
file is located, and therefore the '.dockerignore' file should be located
|
directory is the directory that contains the "docker-compose.yml" file.
|
||||||
alongside the 'docker-compose.yml' file. Matching patterns may be prefixed with
|
|
||||||
the service's directory name (relative to the source directory) in order to
|
|
||||||
apply to that service only (e.g. 'service1/node_modules').
|
|
||||||
|
|
||||||
Previous balena CLI releases (before v12.0.0) also took '.gitignore' files
|
The --multi-dockerignore (-m) option may be used with microservices (multicontainer)
|
||||||
into account. This behavior is deprecated, but may still be enabled with the
|
applications that define a docker-compose.yml file. When this option is used,
|
||||||
--gitignore (-g) option if compatibility is required. This option will be
|
each service subdirectory (defined by the `build` or `build.context` service
|
||||||
removed in the CLI's next major version release (v13).
|
properties in the docker-compose.yml file) is filtered separately according to
|
||||||
|
a .dockerignore file defined in the service subdirectory. If no .dockerignore
|
||||||
|
file exists in a service subdirectory, then only the default .dockerignore
|
||||||
|
patterns (see below) apply for that service subdirectory.
|
||||||
|
|
||||||
When --gitignore (-g) is NOT provided (i.e. when not in v11 compatibility mode),
|
When the --multi-dockerignore (-m) option is used, the .dockerignore file (if
|
||||||
a few "hardcoded" dockerignore patterns are also used and "merged" (in memory)
|
any) defined at the overall project root will be used to filter files and
|
||||||
with the patterns found in the '.dockerignore' file (if any), in the following
|
subdirectories other than service subdirectories. It will not have any effect
|
||||||
order:
|
on service subdirectories, whether or not a service subdirectory defines its
|
||||||
|
own .dockerignore file. Multiple .dockerignore files are not merged or added
|
||||||
|
together, and cannot override or extend other files. This behavior maximises
|
||||||
|
compatibility with the standard docker-compose tool, while still allowing a
|
||||||
|
root .dockerignore file (at the overall project root) to filter files and
|
||||||
|
folders that are outside service subdirectories.
|
||||||
|
|
||||||
|
Balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||||
|
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||||
|
option if compatibility is required. This option is mutually exclusive with
|
||||||
|
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||||
|
release (v13).
|
||||||
|
|
||||||
|
Default .dockerignore patterns
|
||||||
|
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
|
||||||
|
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||||
|
patterns found in the applicable .dockerignore files, in the following order:
|
||||||
|
```
|
||||||
**/.git
|
**/.git
|
||||||
< user's patterns from the '.dockerignore' file, if any >
|
< user's patterns from the applicable '.dockerignore' file, if any >
|
||||||
!**/.balena
|
!**/.balena
|
||||||
!**/.resin
|
!**/.resin
|
||||||
!**/Dockerfile
|
!**/Dockerfile
|
||||||
!**/Dockerfile.*
|
!**/Dockerfile.*
|
||||||
!**/docker-compose.yml
|
!**/docker-compose.yml
|
||||||
|
```
|
||||||
If necessary, the effect of the '**/.git' pattern may be modified by adding
|
These patterns always apply, whether or not .dockerignore files exist in the
|
||||||
"counter patterns" to the '.dockerignore' file, for example '!service1/.git'.
|
project. If necessary, the effect of the `**/.git` pattern may be modified by
|
||||||
For documentation on pattern format, see:
|
adding counter patterns to the applicable .dockerignore file(s), for example
|
||||||
|
`!mysubmodule/.git`. For documentation on pattern format, see:
|
||||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||||
- https://www.npmjs.com/package/@balena/dockerignore
|
- https://www.npmjs.com/package/@balena/dockerignore
|
||||||
|
|
||||||
@ -2263,6 +2301,10 @@ Consider .gitignore files in addition to the .dockerignore file. This reverts
|
|||||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
||||||
until your project can be adapted.
|
until your project can be adapted.
|
||||||
|
|
||||||
|
#### --multi-dockerignore, -m
|
||||||
|
|
||||||
|
Have each service use its own .dockerignore file. See "balena help build".
|
||||||
|
|
||||||
#### --nogitignore, -G
|
#### --nogitignore, -G
|
||||||
|
|
||||||
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
|
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
|
||||||
@ -2351,11 +2393,11 @@ To deploy to an app on which you're a collaborator, use
|
|||||||
When --build is used, all options supported by `balena build` are also supported
|
When --build is used, all options supported by `balena build` are also supported
|
||||||
by this command.
|
by this command.
|
||||||
|
|
||||||
REGISTRY SECRETS
|
REGISTRY SECRETS
|
||||||
The --registry-secrets option specifies a JSON or YAML file containing private
|
The --registry-secrets option specifies a JSON or YAML file containing private
|
||||||
Docker registry usernames and passwords to be used when pulling base images.
|
Docker registry usernames and passwords to be used when pulling base images.
|
||||||
Sample registry-secrets YAML file:
|
Sample registry-secrets YAML file:
|
||||||
|
```
|
||||||
'my-registry-server.com:25000':
|
'my-registry-server.com:25000':
|
||||||
username: ann
|
username: ann
|
||||||
password: hunter2
|
password: hunter2
|
||||||
@ -2365,7 +2407,7 @@ Sample registry-secrets YAML file:
|
|||||||
'eu.gcr.io': # Google Container Registry
|
'eu.gcr.io': # Google Container Registry
|
||||||
username: '_json_key'
|
username: '_json_key'
|
||||||
password: '{escaped contents of the GCR keyfile.json file}'
|
password: '{escaped contents of the GCR keyfile.json file}'
|
||||||
|
```
|
||||||
For a sample project using registry secrets with the Google Container Registry,
|
For a sample project using registry secrets with the Google Container Registry,
|
||||||
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
||||||
|
|
||||||
@ -2373,37 +2415,54 @@ If the --registry-secrets option is not specified, and a secrets.yml or
|
|||||||
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
||||||
this file will be used instead.
|
this file will be used instead.
|
||||||
|
|
||||||
DOCKERIGNORE AND GITIGNORE FILES
|
DOCKERIGNORE AND GITIGNORE FILES
|
||||||
The balena CLI will use a '.dockerignore' file (if any) at the source directory
|
By default, the balena CLI will use a single ".dockerignore" file (if any) at
|
||||||
in order to decide which source files to exclude from the "build context" sent
|
the project root (--source directory) in order to decide which source files to
|
||||||
to balenaCloud, Docker or balenaEngine. In a microservices / multicontainer
|
exclude from the "build context" (tar stream) sent to balenaCloud, Docker daemon
|
||||||
application, the source directory is usually where the 'docker-compose.yml'
|
or balenaEngine. In a microservices (multicontainer) application, the source
|
||||||
file is located, and therefore the '.dockerignore' file should be located
|
directory is the directory that contains the "docker-compose.yml" file.
|
||||||
alongside the 'docker-compose.yml' file. Matching patterns may be prefixed with
|
|
||||||
the service's directory name (relative to the source directory) in order to
|
|
||||||
apply to that service only (e.g. 'service1/node_modules').
|
|
||||||
|
|
||||||
Previous balena CLI releases (before v12.0.0) also took '.gitignore' files
|
The --multi-dockerignore (-m) option may be used with microservices (multicontainer)
|
||||||
into account. This behavior is deprecated, but may still be enabled with the
|
applications that define a docker-compose.yml file. When this option is used,
|
||||||
--gitignore (-g) option if compatibility is required. This option will be
|
each service subdirectory (defined by the `build` or `build.context` service
|
||||||
removed in the CLI's next major version release (v13).
|
properties in the docker-compose.yml file) is filtered separately according to
|
||||||
|
a .dockerignore file defined in the service subdirectory. If no .dockerignore
|
||||||
|
file exists in a service subdirectory, then only the default .dockerignore
|
||||||
|
patterns (see below) apply for that service subdirectory.
|
||||||
|
|
||||||
When --gitignore (-g) is NOT provided (i.e. when not in v11 compatibility mode),
|
When the --multi-dockerignore (-m) option is used, the .dockerignore file (if
|
||||||
a few "hardcoded" dockerignore patterns are also used and "merged" (in memory)
|
any) defined at the overall project root will be used to filter files and
|
||||||
with the patterns found in the '.dockerignore' file (if any), in the following
|
subdirectories other than service subdirectories. It will not have any effect
|
||||||
order:
|
on service subdirectories, whether or not a service subdirectory defines its
|
||||||
|
own .dockerignore file. Multiple .dockerignore files are not merged or added
|
||||||
|
together, and cannot override or extend other files. This behavior maximises
|
||||||
|
compatibility with the standard docker-compose tool, while still allowing a
|
||||||
|
root .dockerignore file (at the overall project root) to filter files and
|
||||||
|
folders that are outside service subdirectories.
|
||||||
|
|
||||||
|
Balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||||
|
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||||
|
option if compatibility is required. This option is mutually exclusive with
|
||||||
|
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||||
|
release (v13).
|
||||||
|
|
||||||
|
Default .dockerignore patterns
|
||||||
|
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
|
||||||
|
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||||
|
patterns found in the applicable .dockerignore files, in the following order:
|
||||||
|
```
|
||||||
**/.git
|
**/.git
|
||||||
< user's patterns from the '.dockerignore' file, if any >
|
< user's patterns from the applicable '.dockerignore' file, if any >
|
||||||
!**/.balena
|
!**/.balena
|
||||||
!**/.resin
|
!**/.resin
|
||||||
!**/Dockerfile
|
!**/Dockerfile
|
||||||
!**/Dockerfile.*
|
!**/Dockerfile.*
|
||||||
!**/docker-compose.yml
|
!**/docker-compose.yml
|
||||||
|
```
|
||||||
If necessary, the effect of the '**/.git' pattern may be modified by adding
|
These patterns always apply, whether or not .dockerignore files exist in the
|
||||||
"counter patterns" to the '.dockerignore' file, for example '!service1/.git'.
|
project. If necessary, the effect of the `**/.git` pattern may be modified by
|
||||||
For documentation on pattern format, see:
|
adding counter patterns to the applicable .dockerignore file(s), for example
|
||||||
|
`!mysubmodule/.git`. For documentation on pattern format, see:
|
||||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||||
- https://www.npmjs.com/package/@balena/dockerignore
|
- https://www.npmjs.com/package/@balena/dockerignore
|
||||||
|
|
||||||
@ -2453,6 +2512,10 @@ Consider .gitignore files in addition to the .dockerignore file. This reverts
|
|||||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
||||||
until your project can be adapted.
|
until your project can be adapted.
|
||||||
|
|
||||||
|
#### --multi-dockerignore, -m
|
||||||
|
|
||||||
|
Have each service use its own .dockerignore file. See "balena help build".
|
||||||
|
|
||||||
#### --nogitignore, -G
|
#### --nogitignore, -G
|
||||||
|
|
||||||
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
|
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
|
||||||
|
@ -24,15 +24,19 @@ import * as compose from '../utils/compose';
|
|||||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||||
import { getBalenaSdk } from '../utils/lazy';
|
import { getBalenaSdk } from '../utils/lazy';
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Opts must be an object with the following keys:
|
* Opts must be an object with the following keys:
|
||||||
|
* app: the app this build is for (optional)
|
||||||
app: the app this build is for (optional)
|
* arch: the architecture to build for
|
||||||
arch: the architecture to build for
|
* deviceType: the device type to build for
|
||||||
deviceType: the device type to build for
|
* buildEmulated
|
||||||
buildEmulated
|
* buildOpts: arguments to forward to docker build command
|
||||||
buildOpts: arguments to forward to docker build command
|
*
|
||||||
*/
|
* @param {import('docker-toolbelt')} docker
|
||||||
|
* @param {import('../utils/logger')} logger
|
||||||
|
* @param {import('../utils/compose-types').ComposeOpts} composeOpts
|
||||||
|
* @param {any} opts
|
||||||
|
*/
|
||||||
const buildProject = function (docker, logger, composeOpts, opts) {
|
const buildProject = function (docker, logger, composeOpts, opts) {
|
||||||
const { loadProject } = require('../utils/compose_ts');
|
const { loadProject } = require('../utils/compose_ts');
|
||||||
return Bluebird.resolve(loadProject(logger, composeOpts))
|
return Bluebird.resolve(loadProject(logger, composeOpts))
|
||||||
@ -63,6 +67,7 @@ const buildProject = function (docker, logger, composeOpts, opts) {
|
|||||||
composeOpts.convertEol,
|
composeOpts.convertEol,
|
||||||
composeOpts.dockerfilePath,
|
composeOpts.dockerfilePath,
|
||||||
composeOpts.nogitignore,
|
composeOpts.nogitignore,
|
||||||
|
composeOpts.multiDockerignore,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
|
@ -25,17 +25,21 @@ import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
|||||||
import { ExpectedError } from '../errors';
|
import { ExpectedError } from '../errors';
|
||||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Opts must be an object with the following keys:
|
* Opts must be an object with the following keys:
|
||||||
|
* app: the application instance to deploy to
|
||||||
app: the application instance to deploy to
|
* image: the image to deploy; optional
|
||||||
image: the image to deploy; optional
|
* dockerfilePath: name of an alternative Dockerfile; optional
|
||||||
dockerfilePath: name of an alternative Dockerfile; optional
|
* shouldPerformBuild
|
||||||
shouldPerformBuild
|
* shouldUploadLogs
|
||||||
shouldUploadLogs
|
* buildEmulated
|
||||||
buildEmulated
|
* buildOpts: arguments to forward to docker build command
|
||||||
buildOpts: arguments to forward to docker build command
|
*
|
||||||
*/
|
* @param {any} docker
|
||||||
|
* @param {import('../utils/logger')} logger
|
||||||
|
* @param {import('../utils/compose-types').ComposeOpts} composeOpts
|
||||||
|
* @param {any} opts
|
||||||
|
*/
|
||||||
const deployProject = function (docker, logger, composeOpts, opts) {
|
const deployProject = function (docker, logger, composeOpts, opts) {
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const doodles = require('resin-doodles');
|
const doodles = require('resin-doodles');
|
||||||
@ -100,6 +104,7 @@ const deployProject = function (docker, logger, composeOpts, opts) {
|
|||||||
composeOpts.convertEol,
|
composeOpts.convertEol,
|
||||||
composeOpts.dockerfilePath,
|
composeOpts.dockerfilePath,
|
||||||
composeOpts.nogitignore,
|
composeOpts.nogitignore,
|
||||||
|
composeOpts.multiDockerignore,
|
||||||
)
|
)
|
||||||
.then((builtImages) => _.keyBy(builtImages, 'serviceName'));
|
.then((builtImages) => _.keyBy(builtImages, 'serviceName'));
|
||||||
})
|
})
|
||||||
|
@ -118,6 +118,7 @@ export const push: CommandDefinition<
|
|||||||
env?: string | string[];
|
env?: string | string[];
|
||||||
'convert-eol'?: boolean;
|
'convert-eol'?: boolean;
|
||||||
'noconvert-eol'?: boolean;
|
'noconvert-eol'?: boolean;
|
||||||
|
'multi-dockerignore'?: boolean;
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
signature: 'push <applicationOrDevice>',
|
signature: 'push <applicationOrDevice>',
|
||||||
@ -276,6 +277,13 @@ export const push: CommandDefinition<
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
{
|
||||||
|
signature: 'multi-dockerignore',
|
||||||
|
alias: 'm',
|
||||||
|
description:
|
||||||
|
'Have each service use its own .dockerignore file. See "balena help push".',
|
||||||
|
boolean: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
signature: 'nogitignore',
|
signature: 'nogitignore',
|
||||||
alias: 'G',
|
alias: 'G',
|
||||||
@ -307,6 +315,11 @@ export const push: CommandDefinition<
|
|||||||
if (appOrDevice == null) {
|
if (appOrDevice == null) {
|
||||||
throw new ExpectedError('You must specify an application or a device');
|
throw new ExpectedError('You must specify an application or a device');
|
||||||
}
|
}
|
||||||
|
if (options.gitignore && options['multi-dockerignore']) {
|
||||||
|
throw new ExpectedError(
|
||||||
|
'The --gitignore and --multi-dockerignore options cannot be used together',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const source = options.source || '.';
|
const source = options.source || '.';
|
||||||
if (process.env.DEBUG) {
|
if (process.env.DEBUG) {
|
||||||
@ -363,6 +376,7 @@ export const push: CommandDefinition<
|
|||||||
const opts = {
|
const opts = {
|
||||||
dockerfilePath,
|
dockerfilePath,
|
||||||
emulated: options.emulated || false,
|
emulated: options.emulated || false,
|
||||||
|
multiDockerignore: options['multi-dockerignore'] || false,
|
||||||
nocache: options.nocache || false,
|
nocache: options.nocache || false,
|
||||||
registrySecrets,
|
registrySecrets,
|
||||||
headless: options.detached || false,
|
headless: options.detached || false,
|
||||||
@ -397,6 +411,7 @@ export const push: CommandDefinition<
|
|||||||
deviceHost: device,
|
deviceHost: device,
|
||||||
dockerfilePath,
|
dockerfilePath,
|
||||||
registrySecrets,
|
registrySecrets,
|
||||||
|
multiDockerignore: options['multi-dockerignore'] || false,
|
||||||
nocache: options.nocache || false,
|
nocache: options.nocache || false,
|
||||||
nogitignore,
|
nogitignore,
|
||||||
noParentCheck: options['noparent-check'] || false,
|
noParentCheck: options['noparent-check'] || false,
|
||||||
|
6
lib/utils/compose-types.d.ts
vendored
6
lib/utils/compose-types.d.ts
vendored
@ -48,6 +48,8 @@ export interface ComposeOpts {
|
|||||||
convertEol: boolean;
|
convertEol: boolean;
|
||||||
dockerfilePath?: string;
|
dockerfilePath?: string;
|
||||||
inlineLogs?: boolean;
|
inlineLogs?: boolean;
|
||||||
|
multiDockerignore: boolean;
|
||||||
|
nogitignore: boolean;
|
||||||
noParentCheck: boolean;
|
noParentCheck: boolean;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
@ -67,7 +69,9 @@ export interface Release {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TarDirectoryOptions {
|
interface TarDirectoryOptions {
|
||||||
|
composition?: Composition;
|
||||||
convertEol?: boolean;
|
convertEol?: boolean;
|
||||||
preFinalizeCallback?: (pack: Pack) => void | Promise<void>;
|
multiDockerignore?: boolean;
|
||||||
nogitignore: boolean;
|
nogitignore: boolean;
|
||||||
|
preFinalizeCallback?: (pack: Pack) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
import * as Bluebird from 'bluebird';
|
import * as Bluebird from 'bluebird';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { ExpectedError } from '../errors';
|
||||||
import { getChalk, stripIndent } from './lazy';
|
import { getChalk, stripIndent } from './lazy';
|
||||||
|
|
||||||
export const appendProjectOptions = (opts) =>
|
export const appendProjectOptions = (opts) =>
|
||||||
@ -72,6 +73,13 @@ export function appendOptions(opts) {
|
|||||||
until your project can be adapted.`,
|
until your project can be adapted.`,
|
||||||
boolean: true,
|
boolean: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
signature: 'multi-dockerignore',
|
||||||
|
alias: 'm',
|
||||||
|
description:
|
||||||
|
'Have each service use its own .dockerignore file. See "balena help build".',
|
||||||
|
boolean: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
signature: 'nogitignore',
|
signature: 'nogitignore',
|
||||||
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
|
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
|
||||||
@ -120,12 +128,19 @@ Source files are not modified.`,
|
|||||||
export function generateOpts(options) {
|
export function generateOpts(options) {
|
||||||
const { promises: fs } = require('fs');
|
const { promises: fs } = require('fs');
|
||||||
const { isV12 } = require('./version');
|
const { isV12 } = require('./version');
|
||||||
|
|
||||||
|
if (options.gitignore && options['multi-dockerignore']) {
|
||||||
|
throw new ExpectedError(
|
||||||
|
'The --gitignore and --multi-dockerignore options cannot be used together',
|
||||||
|
);
|
||||||
|
}
|
||||||
return fs.realpath(options.source || '.').then((projectPath) => ({
|
return fs.realpath(options.source || '.').then((projectPath) => ({
|
||||||
projectName: options.projectName,
|
projectName: options.projectName,
|
||||||
projectPath,
|
projectPath,
|
||||||
inlineLogs: !options.nologs && (!!options.logs || isV12()),
|
inlineLogs: !options.nologs && (!!options.logs || isV12()),
|
||||||
convertEol: isV12() ? !options['noconvert-eol'] : !!options['convert-eol'],
|
convertEol: isV12() ? !options['noconvert-eol'] : !!options['convert-eol'],
|
||||||
dockerfilePath: options.dockerfile,
|
dockerfilePath: options.dockerfile,
|
||||||
|
multiDockerignore: !!options['multi-dockerignore'],
|
||||||
nogitignore: !options.gitignore,
|
nogitignore: !options.gitignore,
|
||||||
noParentCheck: options['noparent-check'],
|
noParentCheck: options['noparent-check'],
|
||||||
}));
|
}));
|
||||||
@ -309,6 +324,7 @@ export function buildProject(
|
|||||||
convertEol,
|
convertEol,
|
||||||
dockerfilePath,
|
dockerfilePath,
|
||||||
nogitignore,
|
nogitignore,
|
||||||
|
multiDockerignore,
|
||||||
) {
|
) {
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const humanize = require('humanize');
|
const humanize = require('humanize');
|
||||||
@ -362,7 +378,12 @@ export function buildProject(
|
|||||||
.then((
|
.then((
|
||||||
needsQemu, // Tar up the directory, ready for the build stream
|
needsQemu, // Tar up the directory, ready for the build stream
|
||||||
) =>
|
) =>
|
||||||
tarDirectory(projectPath, { convertEol, nogitignore })
|
tarDirectory(projectPath, {
|
||||||
|
composition,
|
||||||
|
convertEol,
|
||||||
|
multiDockerignore,
|
||||||
|
nogitignore,
|
||||||
|
})
|
||||||
.then((tarStream) =>
|
.then((tarStream) =>
|
||||||
makeBuildTasks(
|
makeBuildTasks(
|
||||||
composition,
|
composition,
|
||||||
|
@ -57,6 +57,9 @@ const exists = async (filename: string) => {
|
|||||||
|
|
||||||
const compositionFileNames = ['docker-compose.yml', 'docker-compose.yaml'];
|
const compositionFileNames = ['docker-compose.yml', 'docker-compose.yaml'];
|
||||||
|
|
||||||
|
const hr =
|
||||||
|
'----------------------------------------------------------------------';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* high-level function resolving a project and creating a composition out
|
* high-level function resolving a project and creating a composition out
|
||||||
* of it in one go. if image is given, it'll create a default project for
|
* of it in one go. if image is given, it'll create a default project for
|
||||||
@ -105,6 +108,7 @@ export async function loadProject(
|
|||||||
async function resolveProject(
|
async function resolveProject(
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
projectRoot: string,
|
projectRoot: string,
|
||||||
|
quiet = false,
|
||||||
): Promise<[string, string]> {
|
): Promise<[string, string]> {
|
||||||
let composeFileName = '';
|
let composeFileName = '';
|
||||||
let composeFileContents = '';
|
let composeFileContents = '';
|
||||||
@ -122,7 +126,7 @@ async function resolveProject(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!composeFileName) {
|
if (!quiet && !composeFileName) {
|
||||||
logger.logInfo(`No "docker-compose.yml" file found at "${projectRoot}"`);
|
logger.logInfo(`No "docker-compose.yml" file found at "${projectRoot}"`);
|
||||||
}
|
}
|
||||||
return [composeFileName, composeFileContents];
|
return [composeFileName, composeFileContents];
|
||||||
@ -174,6 +178,59 @@ async function loadBuildMetatada(
|
|||||||
return [buildMetadata, metadataPath];
|
return [buildMetadata, metadataPath];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a map of service name to service subdirectory, obtained from the given
|
||||||
|
* composition object. If a composition object is not provided, an attempt will
|
||||||
|
* be made to parse a 'docker-compose.yml' file at the given sourceDir.
|
||||||
|
* Entries will be NOT be returned for subdirectories equal to '.' (e.g. the
|
||||||
|
* 'main' "service" of a single-container application).
|
||||||
|
*
|
||||||
|
* @param sourceDir Project source directory (project root)
|
||||||
|
* @param composition Optional previously parsed composition object
|
||||||
|
*/
|
||||||
|
async function getServiceDirsFromComposition(
|
||||||
|
sourceDir: string,
|
||||||
|
composition?: Composition,
|
||||||
|
): Promise<Dictionary<string>> {
|
||||||
|
const { createProject } = await import('./compose');
|
||||||
|
const serviceDirs: Dictionary<string> = {};
|
||||||
|
if (!composition) {
|
||||||
|
const [, composeStr] = await resolveProject(
|
||||||
|
Logger.getLogger(),
|
||||||
|
sourceDir,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (composeStr) {
|
||||||
|
composition = createProject(sourceDir, composeStr).composition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (composition?.services) {
|
||||||
|
const relPrefix = '.' + path.sep;
|
||||||
|
for (const [serviceName, service] of Object.entries(composition.services)) {
|
||||||
|
let dir =
|
||||||
|
typeof service.build === 'string'
|
||||||
|
? service.build
|
||||||
|
: service.build?.context || '.';
|
||||||
|
// Convert forward slashes to backslashes on Windows
|
||||||
|
dir = path.normalize(dir);
|
||||||
|
// Make sure the path is relative to the project directory
|
||||||
|
if (path.isAbsolute(dir)) {
|
||||||
|
dir = path.relative(sourceDir, dir);
|
||||||
|
}
|
||||||
|
// remove a trailing '/' (or backslash on Windows)
|
||||||
|
dir = dir.endsWith(path.sep) ? dir.slice(0, -1) : dir;
|
||||||
|
// remove './' prefix (or '.\\' on Windows)
|
||||||
|
dir = dir.startsWith(relPrefix) ? dir.slice(2) : dir;
|
||||||
|
// filter out a '.' service directory (e.g. for the 'main' service
|
||||||
|
// of a single-container application)
|
||||||
|
if (dir && dir !== '.') {
|
||||||
|
serviceDirs[serviceName] = dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serviceDirs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a tar stream out of the local filesystem at the given directory,
|
* Create a tar stream out of the local filesystem at the given directory,
|
||||||
* while optionally applying file filters such as '.dockerignore' and
|
* while optionally applying file filters such as '.dockerignore' and
|
||||||
@ -185,15 +242,21 @@ async function loadBuildMetatada(
|
|||||||
export async function tarDirectory(
|
export async function tarDirectory(
|
||||||
dir: string,
|
dir: string,
|
||||||
{
|
{
|
||||||
preFinalizeCallback,
|
composition,
|
||||||
convertEol = false,
|
convertEol = false,
|
||||||
|
multiDockerignore = false,
|
||||||
nogitignore = false,
|
nogitignore = false,
|
||||||
|
preFinalizeCallback,
|
||||||
}: TarDirectoryOptions,
|
}: TarDirectoryOptions,
|
||||||
): Promise<import('stream').Readable> {
|
): Promise<import('stream').Readable> {
|
||||||
(await import('assert')).strict.strictEqual(nogitignore, true);
|
(await import('assert')).strict.strictEqual(nogitignore, true);
|
||||||
const { filterFilesWithDockerignore } = await import('./ignore');
|
const { filterFilesWithDockerignore } = await import('./ignore');
|
||||||
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
|
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
|
||||||
|
|
||||||
|
const serviceDirs = multiDockerignore
|
||||||
|
? await getServiceDirsFromComposition(dir, composition)
|
||||||
|
: {};
|
||||||
|
|
||||||
let readFile: (file: string) => Promise<Buffer>;
|
let readFile: (file: string) => Promise<Buffer>;
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const { readFileWithEolConversion } = require('./eol-conversion');
|
const { readFileWithEolConversion } = require('./eol-conversion');
|
||||||
@ -205,8 +268,8 @@ export async function tarDirectory(
|
|||||||
const {
|
const {
|
||||||
filteredFileList,
|
filteredFileList,
|
||||||
dockerignoreFiles,
|
dockerignoreFiles,
|
||||||
} = await filterFilesWithDockerignore(dir);
|
} = await filterFilesWithDockerignore(dir, serviceDirs);
|
||||||
printDockerignoreWarn(dockerignoreFiles);
|
printDockerignoreWarn(dockerignoreFiles, serviceDirs, multiDockerignore);
|
||||||
for (const fileStats of filteredFileList) {
|
for (const fileStats of filteredFileList) {
|
||||||
pack.entry(
|
pack.entry(
|
||||||
{
|
{
|
||||||
@ -225,35 +288,89 @@ export async function tarDirectory(
|
|||||||
return pack;
|
return pack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print warning messages for unused .dockerignore files, and info messages if
|
||||||
|
* the --multi-dockerignore (-m) option is used in certain circumstances.
|
||||||
|
* @param dockerignoreFiles All .dockerignore files found in the project
|
||||||
|
* @param serviceDirsByService Map of service names to service subdirectories
|
||||||
|
* @param multiDockerignore Whether --multi-dockerignore (-m) was provided
|
||||||
|
*/
|
||||||
export function printDockerignoreWarn(
|
export function printDockerignoreWarn(
|
||||||
dockerignoreFiles: Array<import('./ignore').FileStats>,
|
dockerignoreFiles: Array<import('./ignore').FileStats>,
|
||||||
|
serviceDirsByService: Dictionary<string>,
|
||||||
|
multiDockerignore: boolean,
|
||||||
) {
|
) {
|
||||||
const nonRootFiles = dockerignoreFiles.filter(
|
let rootDockerignore: import('./ignore').FileStats | undefined;
|
||||||
(fileStats: import('./ignore').FileStats) => {
|
const logger = Logger.getLogger();
|
||||||
const dirname = path.dirname(fileStats.relPath);
|
const relPrefix = '.' + path.sep;
|
||||||
return !!dirname && dirname !== '.';
|
const serviceDirs = Object.values(serviceDirsByService || {});
|
||||||
|
// compute a list of unused .dockerignore files
|
||||||
|
const unusedFiles = dockerignoreFiles.filter(
|
||||||
|
(dockerignoreStats: import('./ignore').FileStats) => {
|
||||||
|
let dirname = path.dirname(dockerignoreStats.relPath);
|
||||||
|
dirname = dirname.startsWith(relPrefix) ? dirname.slice(2) : dirname;
|
||||||
|
const isProjectRootDir = !dirname || dirname === '.';
|
||||||
|
if (isProjectRootDir) {
|
||||||
|
rootDockerignore = dockerignoreStats;
|
||||||
|
return false; // a root .dockerignore file is always used
|
||||||
|
}
|
||||||
|
if (multiDockerignore) {
|
||||||
|
for (const serviceDir of serviceDirs) {
|
||||||
|
if (serviceDir === dirname) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (nonRootFiles.length === 0) {
|
const msg: string[] = [];
|
||||||
return;
|
let logFunc = logger.logWarn;
|
||||||
|
// Warn about unused .dockerignore files
|
||||||
|
if (unusedFiles.length) {
|
||||||
|
msg.push(
|
||||||
|
'The following .dockerignore file(s) will not be used:',
|
||||||
|
...unusedFiles.map((fileStats) => `* ${fileStats.filePath}`),
|
||||||
|
);
|
||||||
|
if (multiDockerignore) {
|
||||||
|
msg.push(stripIndent`
|
||||||
|
When --multi-dockerignore (-m) is used, only .dockerignore files at the root of
|
||||||
|
each service's build context (in a microservices/multicontainer application),
|
||||||
|
plus a .dockerignore file at the overall project root, are used.
|
||||||
|
See "balena help ${Logger.command}" for more details.`);
|
||||||
|
} else {
|
||||||
|
msg.push(stripIndent`
|
||||||
|
By default, only one .dockerignore file at the source folder (project root)
|
||||||
|
is used. Microservices (multicontainer) applications may use a separate
|
||||||
|
.dockerignore file for each service with the --multi-dockerignore (-m) option.
|
||||||
|
See "balena help ${Logger.command}" for more details.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No unused .dockerignore files. Print info-level advice in some cases.
|
||||||
|
else if (multiDockerignore) {
|
||||||
|
logFunc = logger.logInfo;
|
||||||
|
// multi-container app with a root .dockerignore file
|
||||||
|
if (serviceDirs.length && rootDockerignore) {
|
||||||
|
msg.push(
|
||||||
|
stripIndent`
|
||||||
|
The --multi-dockerignore option is being used, and a .dockerignore file was
|
||||||
|
found at the project source (root) directory. Note that this file will not
|
||||||
|
be used to filter service subdirectories. See "balena help ${Logger.command}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// single-container app
|
||||||
|
else if (serviceDirs.length === 0) {
|
||||||
|
msg.push(
|
||||||
|
stripIndent`
|
||||||
|
The --multi-dockerignore (-m) option was specified, but it has no effect for
|
||||||
|
single-container (non-microservices) apps. Only one .dockerignore file at the
|
||||||
|
project source (root) directory, if any, is used. See "balena help ${Logger.command}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg.length) {
|
||||||
|
logFunc.call(logger, [' ', hr, ...msg, hr].join('\n'));
|
||||||
}
|
}
|
||||||
const hr =
|
|
||||||
'-------------------------------------------------------------------------------';
|
|
||||||
const msg = [
|
|
||||||
' ',
|
|
||||||
hr,
|
|
||||||
'The following .dockerignore file(s) will not be used:',
|
|
||||||
];
|
|
||||||
msg.push(...nonRootFiles.map((fileStats) => `* ${fileStats.filePath}`));
|
|
||||||
msg.push(stripIndent`
|
|
||||||
Only one .dockerignore file at the source folder (project root) is used.
|
|
||||||
Additional .dockerignore files are disregarded. Microservices (multicontainer)
|
|
||||||
apps should place the .dockerignore file alongside the docker-compose.yml file.
|
|
||||||
See issue: https://github.com/balena-io/balena-cli/issues/1870
|
|
||||||
See also CLI v12 release notes: https://git.io/Jf7hz
|
|
||||||
`);
|
|
||||||
msg.push(hr);
|
|
||||||
Logger.getLogger().logWarn(msg.join('\n'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -270,8 +387,6 @@ export function printGitignoreWarn(
|
|||||||
if (ignoreFiles.length === 0) {
|
if (ignoreFiles.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hr =
|
|
||||||
'-------------------------------------------------------------------------------';
|
|
||||||
const msg = [' ', hr, 'Using file ignore patterns from:'];
|
const msg = [' ', hr, 'Using file ignore patterns from:'];
|
||||||
msg.push(...ignoreFiles.map((e) => `* ${e}`));
|
msg.push(...ignoreFiles.map((e) => `* ${e}`));
|
||||||
if (gitignoreFiles.length) {
|
if (gitignoreFiles.length) {
|
||||||
|
@ -54,6 +54,7 @@ export interface DeviceDeployOptions {
|
|||||||
devicePort?: number;
|
devicePort?: number;
|
||||||
dockerfilePath?: string;
|
dockerfilePath?: string;
|
||||||
registrySecrets: RegistrySecrets;
|
registrySecrets: RegistrySecrets;
|
||||||
|
multiDockerignore: boolean;
|
||||||
nocache: boolean;
|
nocache: boolean;
|
||||||
nogitignore: boolean;
|
nogitignore: boolean;
|
||||||
noParentCheck: boolean;
|
noParentCheck: boolean;
|
||||||
@ -180,6 +181,8 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
|||||||
const project = await loadProject(globalLogger, {
|
const project = await loadProject(globalLogger, {
|
||||||
convertEol: opts.convertEol,
|
convertEol: opts.convertEol,
|
||||||
dockerfilePath: opts.dockerfilePath,
|
dockerfilePath: opts.dockerfilePath,
|
||||||
|
multiDockerignore: opts.multiDockerignore,
|
||||||
|
nogitignore: opts.nogitignore,
|
||||||
noParentCheck: opts.noParentCheck,
|
noParentCheck: opts.noParentCheck,
|
||||||
projectName: 'local',
|
projectName: 'local',
|
||||||
projectPath: opts.source,
|
projectPath: opts.source,
|
||||||
@ -194,7 +197,9 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
|||||||
await checkBuildSecretsRequirements(docker, opts.source);
|
await checkBuildSecretsRequirements(docker, opts.source);
|
||||||
globalLogger.logDebug('Tarring all non-ignored files...');
|
globalLogger.logDebug('Tarring all non-ignored files...');
|
||||||
const tarStream = await tarDirectory(opts.source, {
|
const tarStream = await tarDirectory(opts.source, {
|
||||||
|
composition: project.composition,
|
||||||
convertEol: opts.convertEol,
|
convertEol: opts.convertEol,
|
||||||
|
multiDockerignore: opts.multiDockerignore,
|
||||||
nogitignore: opts.nogitignore,
|
nogitignore: opts.nogitignore,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -407,7 +412,9 @@ export async function rebuildSingleTask(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tarStream = await tarDirectory(source, {
|
const tarStream = await tarDirectory(source, {
|
||||||
|
composition,
|
||||||
convertEol: opts.convertEol,
|
convertEol: opts.convertEol,
|
||||||
|
multiDockerignore: opts.multiDockerignore,
|
||||||
nogitignore: opts.nogitignore,
|
nogitignore: opts.nogitignore,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -249,17 +249,15 @@ async function readDockerIgnoreFile(projectDir: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a list of files (FileStats[]) for the filesystem subtree rooted at
|
* Create an instance of '@balena/dockerignore', initialized with the contents
|
||||||
* projectDir, filtered against a .dockerignore file (if any) also at projectDir,
|
* of a .dockerignore file (if any) found at the given directory argument, plus
|
||||||
* plus a few hardcoded dockerignore patterns.
|
* a set of default/hardcoded patterns.
|
||||||
* @param projectDir Source directory to
|
* @param directory Directory where to look for a .dockerignore file
|
||||||
*/
|
*/
|
||||||
export async function filterFilesWithDockerignore(
|
async function getDockerIgnoreInstance(
|
||||||
projectDir: string,
|
directory: string,
|
||||||
): Promise<{ filteredFileList: FileStats[]; dockerignoreFiles: FileStats[] }> {
|
): Promise<import('@balena/dockerignore').Ignore> {
|
||||||
// path.resolve() also converts forward slashes to backslashes on Windows
|
const dockerIgnoreStr = await readDockerIgnoreFile(directory);
|
||||||
projectDir = path.resolve(projectDir);
|
|
||||||
const dockerIgnoreStr = await readDockerIgnoreFile(projectDir);
|
|
||||||
const $dockerIgnore = (await import('@balena/dockerignore')).default;
|
const $dockerIgnore = (await import('@balena/dockerignore')).default;
|
||||||
const ig = $dockerIgnore({ ignorecase: false });
|
const ig = $dockerIgnore({ ignorecase: false });
|
||||||
|
|
||||||
@ -274,14 +272,60 @@ export async function filterFilesWithDockerignore(
|
|||||||
'!**/Dockerfile.*',
|
'!**/Dockerfile.*',
|
||||||
'!**/docker-compose.yml',
|
'!**/docker-compose.yml',
|
||||||
]);
|
]);
|
||||||
|
return ig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceDirs {
|
||||||
|
[service: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a list of files (FileStats[]) for the filesystem subtree rooted at
|
||||||
|
* projectDir, filtered against the applicable .dockerignore files, including
|
||||||
|
* a few default/hardcoded dockerignore patterns.
|
||||||
|
* @param projectDir Source directory to
|
||||||
|
* @param serviceDirsByService Map of service names to their subdirectories.
|
||||||
|
* The service directory names/paths must be relative to the project root dir
|
||||||
|
* and be "normalized" (path.normalize()) before the call to this function:
|
||||||
|
* they should use backslashes on Windows, not contain '.' or '..' segments and
|
||||||
|
* not contain multiple consecutive path separators like '//'. Also, relative
|
||||||
|
* paths must not start with './' (e.g. 'a/b' instead of './a/b').
|
||||||
|
*/
|
||||||
|
export async function filterFilesWithDockerignore(
|
||||||
|
projectDir: string,
|
||||||
|
serviceDirsByService?: ServiceDirs,
|
||||||
|
): Promise<{ filteredFileList: FileStats[]; dockerignoreFiles: FileStats[] }> {
|
||||||
|
// path.resolve() also converts forward slashes to backslashes on Windows
|
||||||
|
projectDir = path.resolve(projectDir);
|
||||||
|
// ignoreByDir stores an instance of the dockerignore filter for each service dir
|
||||||
|
const ignoreByDir: {
|
||||||
|
[serviceDir: string]: import('@balena/dockerignore').Ignore;
|
||||||
|
} = {
|
||||||
|
'.': await getDockerIgnoreInstance(projectDir),
|
||||||
|
};
|
||||||
|
const serviceDirs: string[] = Object.values(serviceDirsByService || {})
|
||||||
|
// filter out the project source/root dir
|
||||||
|
.filter((dir) => dir && dir !== '.')
|
||||||
|
// add a trailing '/' (or '\' on Windows) to the path
|
||||||
|
.map((dir) => (dir.endsWith(path.sep) ? dir : dir + path.sep));
|
||||||
|
|
||||||
|
for (const serviceDir of serviceDirs) {
|
||||||
|
ignoreByDir[serviceDir] = await getDockerIgnoreInstance(
|
||||||
|
path.join(projectDir, serviceDir),
|
||||||
|
);
|
||||||
|
}
|
||||||
const files = await listFiles(projectDir);
|
const files = await listFiles(projectDir);
|
||||||
const dockerignoreFiles: FileStats[] = [];
|
const dockerignoreFiles: FileStats[] = [];
|
||||||
const filteredFileList = files.filter((file: FileStats) => {
|
const filteredFileList = files.filter((file: FileStats) => {
|
||||||
if (path.basename(file.relPath) === '.dockerignore') {
|
if (path.basename(file.relPath) === '.dockerignore') {
|
||||||
dockerignoreFiles.push(file);
|
dockerignoreFiles.push(file);
|
||||||
}
|
}
|
||||||
return !ig.ignores(file.relPath);
|
for (const dir of serviceDirs) {
|
||||||
|
if (file.relPath.startsWith(dir)) {
|
||||||
|
return !ignoreByDir[dir].ignores(file.relPath.substring(dir.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !ignoreByDir['.'].ignores(file.relPath);
|
||||||
});
|
});
|
||||||
return { filteredFileList, dockerignoreFiles };
|
return { filteredFileList, dockerignoreFiles };
|
||||||
}
|
}
|
||||||
|
@ -45,11 +45,11 @@ export const balenaAsciiArt = `\
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const registrySecretsHelp = `\
|
export const registrySecretsHelp = `\
|
||||||
REGISTRY SECRETS
|
REGISTRY SECRETS
|
||||||
The --registry-secrets option specifies a JSON or YAML file containing private
|
The --registry-secrets option specifies a JSON or YAML file containing private
|
||||||
Docker registry usernames and passwords to be used when pulling base images.
|
Docker registry usernames and passwords to be used when pulling base images.
|
||||||
Sample registry-secrets YAML file:
|
Sample registry-secrets YAML file:
|
||||||
|
\`\`\`
|
||||||
'my-registry-server.com:25000':
|
'my-registry-server.com:25000':
|
||||||
username: ann
|
username: ann
|
||||||
password: hunter2
|
password: hunter2
|
||||||
@ -59,7 +59,7 @@ Sample registry-secrets YAML file:
|
|||||||
'eu.gcr.io': # Google Container Registry
|
'eu.gcr.io': # Google Container Registry
|
||||||
username: '_json_key'
|
username: '_json_key'
|
||||||
password: '{escaped contents of the GCR keyfile.json file}'
|
password: '{escaped contents of the GCR keyfile.json file}'
|
||||||
|
\`\`\`
|
||||||
For a sample project using registry secrets with the Google Container Registry,
|
For a sample project using registry secrets with the Google Container Registry,
|
||||||
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
check: https://github.com/balena-io-playground/sample-gcr-registry-secrets
|
||||||
|
|
||||||
@ -68,36 +68,53 @@ secrets.json file exists in the balena directory (usually $HOME/.balena),
|
|||||||
this file will be used instead.`;
|
this file will be used instead.`;
|
||||||
|
|
||||||
export const dockerignoreHelp = `\
|
export const dockerignoreHelp = `\
|
||||||
DOCKERIGNORE AND GITIGNORE FILES
|
DOCKERIGNORE AND GITIGNORE FILES
|
||||||
The balena CLI will use a '.dockerignore' file (if any) at the source directory
|
By default, the balena CLI will use a single ".dockerignore" file (if any) at
|
||||||
in order to decide which source files to exclude from the "build context" sent
|
the project root (--source directory) in order to decide which source files to
|
||||||
to balenaCloud, Docker or balenaEngine. In a microservices / multicontainer
|
exclude from the "build context" (tar stream) sent to balenaCloud, Docker daemon
|
||||||
application, the source directory is usually where the 'docker-compose.yml'
|
or balenaEngine. In a microservices (multicontainer) application, the source
|
||||||
file is located, and therefore the '.dockerignore' file should be located
|
directory is the directory that contains the "docker-compose.yml" file.
|
||||||
alongside the 'docker-compose.yml' file. Matching patterns may be prefixed with
|
|
||||||
the service's directory name (relative to the source directory) in order to
|
|
||||||
apply to that service only (e.g. 'service1/node_modules').
|
|
||||||
|
|
||||||
Previous balena CLI releases (before v12.0.0) also took '.gitignore' files
|
The --multi-dockerignore (-m) option may be used with microservices (multicontainer)
|
||||||
into account. This behavior is deprecated, but may still be enabled with the
|
applications that define a docker-compose.yml file. When this option is used,
|
||||||
--gitignore (-g) option if compatibility is required. This option will be
|
each service subdirectory (defined by the \`build\` or \`build.context\` service
|
||||||
removed in the CLI's next major version release (v13).
|
properties in the docker-compose.yml file) is filtered separately according to
|
||||||
|
a .dockerignore file defined in the service subdirectory. If no .dockerignore
|
||||||
|
file exists in a service subdirectory, then only the default .dockerignore
|
||||||
|
patterns (see below) apply for that service subdirectory.
|
||||||
|
|
||||||
When --gitignore (-g) is NOT provided (i.e. when not in v11 compatibility mode),
|
When the --multi-dockerignore (-m) option is used, the .dockerignore file (if
|
||||||
a few "hardcoded" dockerignore patterns are also used and "merged" (in memory)
|
any) defined at the overall project root will be used to filter files and
|
||||||
with the patterns found in the '.dockerignore' file (if any), in the following
|
subdirectories other than service subdirectories. It will not have any effect
|
||||||
order:
|
on service subdirectories, whether or not a service subdirectory defines its
|
||||||
|
own .dockerignore file. Multiple .dockerignore files are not merged or added
|
||||||
|
together, and cannot override or extend other files. This behavior maximises
|
||||||
|
compatibility with the standard docker-compose tool, while still allowing a
|
||||||
|
root .dockerignore file (at the overall project root) to filter files and
|
||||||
|
folders that are outside service subdirectories.
|
||||||
|
|
||||||
|
Balena CLI releases older than v12.0.0 also took .gitignore files into account.
|
||||||
|
This behavior is deprecated, but may still be enabled with the --gitignore (-g)
|
||||||
|
option if compatibility is required. This option is mutually exclusive with
|
||||||
|
--multi-dockerignore (-m) and will be removed in the CLI's next major version
|
||||||
|
release (v13).
|
||||||
|
|
||||||
|
Default .dockerignore patterns
|
||||||
|
When --gitignore (-g) is NOT used (i.e. when not in v11 compatibility mode), a
|
||||||
|
few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||||
|
patterns found in the applicable .dockerignore files, in the following order:
|
||||||
|
\`\`\`
|
||||||
**/.git
|
**/.git
|
||||||
< user's patterns from the '.dockerignore' file, if any >
|
< user's patterns from the applicable '.dockerignore' file, if any >
|
||||||
!**/.balena
|
!**/.balena
|
||||||
!**/.resin
|
!**/.resin
|
||||||
!**/Dockerfile
|
!**/Dockerfile
|
||||||
!**/Dockerfile.*
|
!**/Dockerfile.*
|
||||||
!**/docker-compose.yml
|
!**/docker-compose.yml
|
||||||
|
\`\`\`
|
||||||
If necessary, the effect of the '**/.git' pattern may be modified by adding
|
These patterns always apply, whether or not .dockerignore files exist in the
|
||||||
"counter patterns" to the '.dockerignore' file, for example '!service1/.git'.
|
project. If necessary, the effect of the \`**/.git\` pattern may be modified by
|
||||||
For documentation on pattern format, see:
|
adding counter patterns to the applicable .dockerignore file(s), for example
|
||||||
|
\`!mysubmodule/.git\`. For documentation on pattern format, see:
|
||||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||||
- https://www.npmjs.com/package/@balena/dockerignore`;
|
- https://www.npmjs.com/package/@balena/dockerignore`;
|
||||||
|
@ -43,6 +43,7 @@ export interface BuildOpts {
|
|||||||
registrySecrets: RegistrySecrets;
|
registrySecrets: RegistrySecrets;
|
||||||
headless: boolean;
|
headless: boolean;
|
||||||
convertEol: boolean;
|
convertEol: boolean;
|
||||||
|
multiDockerignore: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoteBuild {
|
export interface RemoteBuild {
|
||||||
@ -306,6 +307,7 @@ async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
|
|||||||
return await tarDirectory(path.resolve(build.source), {
|
return await tarDirectory(path.resolve(build.source), {
|
||||||
preFinalizeCallback: preFinalizeCb,
|
preFinalizeCallback: preFinalizeCb,
|
||||||
convertEol: build.opts.convertEol,
|
convertEol: build.opts.convertEol,
|
||||||
|
multiDockerignore: build.opts.multiDockerignore,
|
||||||
nogitignore: build.nogitignore,
|
nogitignore: build.nogitignore,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -61,6 +61,9 @@ const commonComposeQueryParams = [
|
|||||||
['labels', ''],
|
['labels', ''],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const hr =
|
||||||
|
'----------------------------------------------------------------------';
|
||||||
|
|
||||||
// "itSS" means "it() Skip Standalone"
|
// "itSS" means "it() Skip Standalone"
|
||||||
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
|
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
|
||||||
|
|
||||||
@ -172,15 +175,14 @@ describe('balena build', function () {
|
|||||||
'[Info] Building for rpi/raspberry-pi',
|
'[Info] Building for rpi/raspberry-pi',
|
||||||
'[Info] Emulation is enabled',
|
'[Info] Emulation is enabled',
|
||||||
...[
|
...[
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
`[Warn] ${hr}`,
|
||||||
'[Warn] The following .dockerignore file(s) will not be used:',
|
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||||
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
||||||
'[Warn] Only one .dockerignore file at the source folder (project root) is used.',
|
'[Warn] By default, only one .dockerignore file at the source folder (project root)',
|
||||||
'[Warn] Additional .dockerignore files are disregarded. Microservices (multicontainer)',
|
'[Warn] is used. Microservices (multicontainer) applications may use a separate',
|
||||||
'[Warn] apps should place the .dockerignore file alongside the docker-compose.yml file.',
|
'[Warn] .dockerignore file for each service with the --multi-dockerignore (-m) option.',
|
||||||
'[Warn] See issue: https://github.com/balena-io/balena-cli/issues/1870',
|
'[Warn] See "balena help build" for more details.',
|
||||||
'[Warn] See also CLI v12 release notes: https://git.io/Jf7hz',
|
`[Warn] ${hr}`,
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
|
||||||
],
|
],
|
||||||
'[Build] main Step 1/4 : FROM busybox',
|
'[Build] main Step 1/4 : FROM busybox',
|
||||||
'[Success] Build succeeded!',
|
'[Success] Build succeeded!',
|
||||||
@ -230,7 +232,7 @@ describe('balena build', function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create the expected tar stream (single container, --[no]convert-eol)', async () => {
|
it('should create the expected tar stream (single container, --[no]convert-eol, --multi-dockerignore)', async () => {
|
||||||
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
||||||
const expectedFiles: ExpectedTarStreamFiles = {
|
const expectedFiles: ExpectedTarStreamFiles = {
|
||||||
'src/.dockerignore': { fileSize: 16, type: 'file' },
|
'src/.dockerignore': { fileSize: 16, type: 'file' },
|
||||||
@ -252,15 +254,14 @@ describe('balena build', function () {
|
|||||||
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
|
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
|
||||||
`[Info] Creating default composition with source: "${projectPath}"`,
|
`[Info] Creating default composition with source: "${projectPath}"`,
|
||||||
...[
|
...[
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
`[Warn] ${hr}`,
|
||||||
'[Warn] The following .dockerignore file(s) will not be used:',
|
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||||
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
||||||
'[Warn] Only one .dockerignore file at the source folder (project root) is used.',
|
'[Warn] When --multi-dockerignore (-m) is used, only .dockerignore files at the root of',
|
||||||
'[Warn] Additional .dockerignore files are disregarded. Microservices (multicontainer)',
|
"[Warn] each service's build context (in a microservices/multicontainer application),",
|
||||||
'[Warn] apps should place the .dockerignore file alongside the docker-compose.yml file.',
|
'[Warn] plus a .dockerignore file at the overall project root, are used.',
|
||||||
'[Warn] See issue: https://github.com/balena-io/balena-cli/issues/1870',
|
'[Warn] See "balena help build" for more details.',
|
||||||
'[Warn] See also CLI v12 release notes: https://git.io/Jf7hz',
|
`[Warn] ${hr}`,
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
|
||||||
],
|
],
|
||||||
'[Build] main Step 1/4 : FROM busybox',
|
'[Build] main Step 1/4 : FROM busybox',
|
||||||
];
|
];
|
||||||
@ -273,7 +274,7 @@ describe('balena build', function () {
|
|||||||
}
|
}
|
||||||
docker.expectGetInfo({});
|
docker.expectGetInfo({});
|
||||||
await testDockerBuildStream({
|
await testDockerBuildStream({
|
||||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --noconvert-eol`,
|
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --noconvert-eol -m`,
|
||||||
dockerMock: docker,
|
dockerMock: docker,
|
||||||
expectedFilesByService: { main: expectedFiles },
|
expectedFilesByService: { main: expectedFiles },
|
||||||
expectedQueryParamsByService: { main: commonQueryParams },
|
expectedQueryParamsByService: { main: commonQueryParams },
|
||||||
@ -304,7 +305,93 @@ describe('balena build', function () {
|
|||||||
'file1.sh': { fileSize: 12, type: 'file' },
|
'file1.sh': { fileSize: 12, type: 'file' },
|
||||||
},
|
},
|
||||||
service2: {
|
service2: {
|
||||||
'.dockerignore': { fileSize: 14, type: 'file' },
|
'.dockerignore': { fileSize: 12, type: 'file' },
|
||||||
|
'Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||||
|
'file2-crlf.sh': {
|
||||||
|
fileSize: isWindows ? 12 : 14,
|
||||||
|
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
'src/file1.sh': { fileSize: 12, type: 'file' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const responseFilename = 'build-POST.json';
|
||||||
|
const responseBody = await fs.readFile(
|
||||||
|
path.join(dockerResponsePath, responseFilename),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const expectedQueryParamsByService = {
|
||||||
|
service1: [
|
||||||
|
['t', '${tag}'],
|
||||||
|
[
|
||||||
|
'buildargs',
|
||||||
|
'{"MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable","SERVICE1_VAR":"This is a service specific variable"}',
|
||||||
|
],
|
||||||
|
['labels', ''],
|
||||||
|
],
|
||||||
|
service2: [...commonComposeQueryParams, ['dockerfile', 'Dockerfile-alt']],
|
||||||
|
};
|
||||||
|
const expectedResponseLines: string[] = [
|
||||||
|
...commonResponseLines[responseFilename],
|
||||||
|
...[
|
||||||
|
'[Build] service1 Step 1/4 : FROM busybox',
|
||||||
|
'[Build] service2 Step 1/4 : FROM busybox',
|
||||||
|
],
|
||||||
|
...[
|
||||||
|
`[Warn] ${hr}`,
|
||||||
|
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||||
|
`[Warn] * ${path.join(projectPath, 'service2', '.dockerignore')}`,
|
||||||
|
'[Warn] By default, only one .dockerignore file at the source folder (project root)',
|
||||||
|
'[Warn] is used. Microservices (multicontainer) applications may use a separate',
|
||||||
|
'[Warn] .dockerignore file for each service with the --multi-dockerignore (-m) option.',
|
||||||
|
'[Warn] See "balena help build" for more details.',
|
||||||
|
`[Warn] ${hr}`,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if (isWindows) {
|
||||||
|
expectedResponseLines.push(
|
||||||
|
`[Info] Converting line endings CRLF -> LF for file: ${path.join(
|
||||||
|
projectPath,
|
||||||
|
'service2',
|
||||||
|
'file2-crlf.sh',
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
docker.expectGetInfo({});
|
||||||
|
await testDockerBuildStream({
|
||||||
|
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -G`,
|
||||||
|
dockerMock: docker,
|
||||||
|
expectedFilesByService,
|
||||||
|
expectedQueryParamsByService,
|
||||||
|
expectedResponseLines,
|
||||||
|
projectPath,
|
||||||
|
responseBody,
|
||||||
|
responseCode: 200,
|
||||||
|
services: ['service1', 'service2'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the expected tar stream (docker-compose, --multi-dockerignore)', async () => {
|
||||||
|
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
|
||||||
|
const service1Dockerfile = (
|
||||||
|
await fs.readFile(
|
||||||
|
path.join(projectPath, 'service1', 'Dockerfile.template'),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
).replace('%%BALENA_MACHINE_NAME%%', 'nuc');
|
||||||
|
const expectedFilesByService: ExpectedTarStreamFilesByService = {
|
||||||
|
service1: {
|
||||||
|
Dockerfile: {
|
||||||
|
contents: service1Dockerfile,
|
||||||
|
fileSize: service1Dockerfile.length,
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
'Dockerfile.template': { fileSize: 144, type: 'file' },
|
||||||
|
'file1.sh': { fileSize: 12, type: 'file' },
|
||||||
|
'test-ignore.txt': { fileSize: 12, type: 'file' },
|
||||||
|
},
|
||||||
|
service2: {
|
||||||
|
'.dockerignore': { fileSize: 12, type: 'file' },
|
||||||
'Dockerfile-alt': { fileSize: 40, type: 'file' },
|
'Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||||
'file2-crlf.sh': {
|
'file2-crlf.sh': {
|
||||||
fileSize: isWindows ? 12 : 14,
|
fileSize: isWindows ? 12 : 14,
|
||||||
@ -336,14 +423,11 @@ describe('balena build', function () {
|
|||||||
'[Build] service2 Step 1/4 : FROM busybox',
|
'[Build] service2 Step 1/4 : FROM busybox',
|
||||||
],
|
],
|
||||||
...[
|
...[
|
||||||
'[Warn] The following .dockerignore file(s) will not be used:',
|
`[Info] ${hr}`,
|
||||||
`[Warn] * ${path.join(projectPath, 'service2', '.dockerignore')}`,
|
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
|
||||||
'[Warn] Only one .dockerignore file at the source folder (project root) is used.',
|
'[Info] found at the project source (root) directory. Note that this file will not',
|
||||||
'[Warn] Additional .dockerignore files are disregarded. Microservices (multicontainer)',
|
'[Info] be used to filter service subdirectories. See "balena help build".',
|
||||||
'[Warn] apps should place the .dockerignore file alongside the docker-compose.yml file.',
|
`[Info] ${hr}`,
|
||||||
'[Warn] See issue: https://github.com/balena-io/balena-cli/issues/1870',
|
|
||||||
'[Warn] See also CLI v12 release notes: https://git.io/Jf7hz',
|
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
@ -357,7 +441,7 @@ describe('balena build', function () {
|
|||||||
}
|
}
|
||||||
docker.expectGetInfo({});
|
docker.expectGetInfo({});
|
||||||
await testDockerBuildStream({
|
await testDockerBuildStream({
|
||||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -G`,
|
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m`,
|
||||||
dockerMock: docker,
|
dockerMock: docker,
|
||||||
expectedFilesByService,
|
expectedFilesByService,
|
||||||
expectedQueryParamsByService,
|
expectedQueryParamsByService,
|
||||||
|
@ -27,7 +27,10 @@ import { BalenaAPIMock } from '../balena-api-mock';
|
|||||||
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
||||||
import { DockerMock, dockerResponsePath } from '../docker-mock';
|
import { DockerMock, dockerResponsePath } from '../docker-mock';
|
||||||
import { cleanOutput, runCommand, switchSentry } from '../helpers';
|
import { cleanOutput, runCommand, switchSentry } from '../helpers';
|
||||||
import { ExpectedTarStreamFiles } from '../projects';
|
import {
|
||||||
|
ExpectedTarStreamFiles,
|
||||||
|
ExpectedTarStreamFilesByService,
|
||||||
|
} from '../projects';
|
||||||
|
|
||||||
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
|
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
|
||||||
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
|
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
|
||||||
@ -38,7 +41,7 @@ const commonResponseLines = {
|
|||||||
'[Info] Docker Desktop detected (daemon architecture: "x86_64")',
|
'[Info] Docker Desktop detected (daemon architecture: "x86_64")',
|
||||||
'[Info] Docker itself will determine and enable architecture emulation if required,',
|
'[Info] Docker itself will determine and enable architecture emulation if required,',
|
||||||
'[Info] without balena-cli intervention and regardless of the --emulated option.',
|
'[Info] without balena-cli intervention and regardless of the --emulated option.',
|
||||||
'[Build] main Step 1/4 : FROM busybox',
|
// '[Build] main Step 1/4 : FROM busybox',
|
||||||
'[Info] Creating release...',
|
'[Info] Creating release...',
|
||||||
'[Info] Pushing images to registry...',
|
'[Info] Pushing images to registry...',
|
||||||
'[Info] Saving release...',
|
'[Info] Saving release...',
|
||||||
@ -53,6 +56,18 @@ const commonQueryParams = [
|
|||||||
['labels', ''],
|
['labels', ''],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const commonComposeQueryParams = [
|
||||||
|
['t', '${tag}'],
|
||||||
|
[
|
||||||
|
'buildargs',
|
||||||
|
'{"MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable"}',
|
||||||
|
],
|
||||||
|
['labels', ''],
|
||||||
|
];
|
||||||
|
|
||||||
|
const hr =
|
||||||
|
'----------------------------------------------------------------------';
|
||||||
|
|
||||||
describe('balena deploy', function () {
|
describe('balena deploy', function () {
|
||||||
let api: BalenaAPIMock;
|
let api: BalenaAPIMock;
|
||||||
let docker: DockerMock;
|
let docker: DockerMock;
|
||||||
@ -73,8 +88,8 @@ describe('balena deploy', function () {
|
|||||||
api.expectGetAuth();
|
api.expectGetAuth();
|
||||||
api.expectPostImage();
|
api.expectPostImage();
|
||||||
api.expectPostImageIsPartOfRelease();
|
api.expectPostImageIsPartOfRelease();
|
||||||
api.expectPostImageLabel();
|
|
||||||
|
|
||||||
|
docker.expectGetImages();
|
||||||
docker.expectGetPing();
|
docker.expectGetPing();
|
||||||
docker.expectGetInfo({});
|
docker.expectGetInfo({});
|
||||||
docker.expectGetVersion({ persist: true });
|
docker.expectGetVersion({ persist: true });
|
||||||
@ -112,15 +127,14 @@ describe('balena deploy', function () {
|
|||||||
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
|
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
|
||||||
`[Info] Creating default composition with source: "${projectPath}"`,
|
`[Info] Creating default composition with source: "${projectPath}"`,
|
||||||
...[
|
...[
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
`[Warn] ${hr}`,
|
||||||
'[Warn] The following .dockerignore file(s) will not be used:',
|
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||||
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
||||||
'[Warn] Only one .dockerignore file at the source folder (project root) is used.',
|
'[Warn] By default, only one .dockerignore file at the source folder (project root)',
|
||||||
'[Warn] Additional .dockerignore files are disregarded. Microservices (multicontainer)',
|
'[Warn] is used. Microservices (multicontainer) applications may use a separate',
|
||||||
'[Warn] apps should place the .dockerignore file alongside the docker-compose.yml file.',
|
'[Warn] .dockerignore file for each service with the --multi-dockerignore (-m) option.',
|
||||||
'[Warn] See issue: https://github.com/balena-io/balena-cli/issues/1870',
|
'[Warn] See "balena help deploy" for more details.',
|
||||||
'[Warn] See also CLI v12 release notes: https://git.io/Jf7hz',
|
`[Warn] ${hr}`,
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
@ -132,6 +146,7 @@ describe('balena deploy', function () {
|
|||||||
|
|
||||||
api.expectPatchImage({});
|
api.expectPatchImage({});
|
||||||
api.expectPatchRelease({});
|
api.expectPatchRelease({});
|
||||||
|
api.expectPostImageLabel();
|
||||||
|
|
||||||
await testDockerBuildStream({
|
await testDockerBuildStream({
|
||||||
commandLine: `deploy testApp --build --source ${projectPath} -G`,
|
commandLine: `deploy testApp --build --source ${projectPath} -G`,
|
||||||
@ -189,6 +204,7 @@ describe('balena deploy', function () {
|
|||||||
expect(releaseBody.status).to.equal('failed');
|
expect(releaseBody.status).to.equal('failed');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
api.expectPostImageLabel();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sentryStatus = await switchSentry(false);
|
sentryStatus = await switchSentry(false);
|
||||||
@ -213,6 +229,98 @@ describe('balena deploy', function () {
|
|||||||
process.exit.restore();
|
process.exit.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create the expected tar stream (docker-compose, --multi-dockerignore)', async () => {
|
||||||
|
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
|
||||||
|
const service1Dockerfile = (
|
||||||
|
await fs.readFile(
|
||||||
|
path.join(projectPath, 'service1', 'Dockerfile.template'),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
).replace('%%BALENA_MACHINE_NAME%%', 'raspberrypi3');
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
`Dockerfile.template (replaced) length=${service1Dockerfile.length}`,
|
||||||
|
);
|
||||||
|
console.error(service1Dockerfile);
|
||||||
|
|
||||||
|
const expectedFilesByService: ExpectedTarStreamFilesByService = {
|
||||||
|
service1: {
|
||||||
|
Dockerfile: {
|
||||||
|
contents: service1Dockerfile,
|
||||||
|
fileSize: service1Dockerfile.length,
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
'Dockerfile.template': { fileSize: 144, type: 'file' },
|
||||||
|
'file1.sh': { fileSize: 12, type: 'file' },
|
||||||
|
'test-ignore.txt': { fileSize: 12, type: 'file' },
|
||||||
|
},
|
||||||
|
service2: {
|
||||||
|
'.dockerignore': { fileSize: 12, type: 'file' },
|
||||||
|
'Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||||
|
'file2-crlf.sh': {
|
||||||
|
fileSize: isWindows ? 12 : 14,
|
||||||
|
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const responseFilename = 'build-POST.json';
|
||||||
|
const responseBody = await fs.readFile(
|
||||||
|
path.join(dockerResponsePath, responseFilename),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const expectedQueryParamsByService = {
|
||||||
|
service1: [
|
||||||
|
['t', '${tag}'],
|
||||||
|
[
|
||||||
|
'buildargs',
|
||||||
|
'{"MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable","SERVICE1_VAR":"This is a service specific variable"}',
|
||||||
|
],
|
||||||
|
['labels', ''],
|
||||||
|
],
|
||||||
|
service2: [...commonComposeQueryParams, ['dockerfile', 'Dockerfile-alt']],
|
||||||
|
};
|
||||||
|
const expectedResponseLines: string[] = [
|
||||||
|
...commonResponseLines[responseFilename],
|
||||||
|
...[
|
||||||
|
'[Build] service1 Step 1/4 : FROM busybox',
|
||||||
|
'[Build] service2 Step 1/4 : FROM busybox',
|
||||||
|
],
|
||||||
|
...[
|
||||||
|
`[Info] ${hr}`,
|
||||||
|
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
|
||||||
|
'[Info] found at the project source (root) directory. Note that this file will not',
|
||||||
|
'[Info] be used to filter service subdirectories. See "balena help deploy".',
|
||||||
|
`[Info] ${hr}`,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if (isWindows) {
|
||||||
|
expectedResponseLines.push(
|
||||||
|
`[Info] Converting line endings CRLF -> LF for file: ${path.join(
|
||||||
|
projectPath,
|
||||||
|
'service2',
|
||||||
|
'file2-crlf.sh',
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// docker.expectGetImages();
|
||||||
|
api.expectPatchImage({});
|
||||||
|
api.expectPatchRelease({});
|
||||||
|
|
||||||
|
await testDockerBuildStream({
|
||||||
|
commandLine: `deploy testApp --build --source ${projectPath} --multi-dockerignore`,
|
||||||
|
dockerMock: docker,
|
||||||
|
expectedFilesByService,
|
||||||
|
expectedQueryParamsByService,
|
||||||
|
expectedResponseLines,
|
||||||
|
projectPath,
|
||||||
|
responseBody,
|
||||||
|
responseCode: 200,
|
||||||
|
services: ['service1', 'service2'],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('balena deploy: project validation', function () {
|
describe('balena deploy: project validation', function () {
|
||||||
|
@ -77,6 +77,9 @@ const commonQueryParams = [
|
|||||||
['headless', 'false'],
|
['headless', 'false'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const hr =
|
||||||
|
'----------------------------------------------------------------------';
|
||||||
|
|
||||||
describe('balena push', function () {
|
describe('balena push', function () {
|
||||||
let api: BalenaAPIMock;
|
let api: BalenaAPIMock;
|
||||||
let builder: BuilderMock;
|
let builder: BuilderMock;
|
||||||
@ -126,14 +129,14 @@ describe('balena push', function () {
|
|||||||
const expectedResponseLines = [
|
const expectedResponseLines = [
|
||||||
...commonResponseLines[responseFilename],
|
...commonResponseLines[responseFilename],
|
||||||
...[
|
...[
|
||||||
|
`[Warn] ${hr}`,
|
||||||
'[Warn] The following .dockerignore file(s) will not be used:',
|
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||||
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
||||||
'[Warn] Only one .dockerignore file at the source folder (project root) is used.',
|
'[Warn] By default, only one .dockerignore file at the source folder (project root)',
|
||||||
'[Warn] Additional .dockerignore files are disregarded. Microservices (multicontainer)',
|
'[Warn] is used. Microservices (multicontainer) applications may use a separate',
|
||||||
'[Warn] apps should place the .dockerignore file alongside the docker-compose.yml file.',
|
'[Warn] .dockerignore file for each service with the --multi-dockerignore (-m) option.',
|
||||||
'[Warn] See issue: https://github.com/balena-io/balena-cli/issues/1870',
|
'[Warn] See "balena help push" for more details.',
|
||||||
'[Warn] See also CLI v12 release notes: https://git.io/Jf7hz',
|
`[Warn] ${hr}`,
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
@ -173,14 +176,14 @@ describe('balena push', function () {
|
|||||||
const expectedResponseLines = [
|
const expectedResponseLines = [
|
||||||
...commonResponseLines[responseFilename],
|
...commonResponseLines[responseFilename],
|
||||||
...[
|
...[
|
||||||
|
`[Warn] ${hr}`,
|
||||||
'[Warn] The following .dockerignore file(s) will not be used:',
|
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||||
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
||||||
'[Warn] Only one .dockerignore file at the source folder (project root) is used.',
|
'[Warn] By default, only one .dockerignore file at the source folder (project root)',
|
||||||
'[Warn] Additional .dockerignore files are disregarded. Microservices (multicontainer)',
|
'[Warn] is used. Microservices (multicontainer) applications may use a separate',
|
||||||
'[Warn] apps should place the .dockerignore file alongside the docker-compose.yml file.',
|
'[Warn] .dockerignore file for each service with the --multi-dockerignore (-m) option.',
|
||||||
'[Warn] See issue: https://github.com/balena-io/balena-cli/issues/1870',
|
'[Warn] See "balena help push" for more details.',
|
||||||
'[Warn] See also CLI v12 release notes: https://git.io/Jf7hz',
|
`[Warn] ${hr}`,
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
const expectedQueryParams = commonQueryParams.map((i) =>
|
const expectedQueryParams = commonQueryParams.map((i) =>
|
||||||
@ -220,14 +223,14 @@ describe('balena push', function () {
|
|||||||
const expectedResponseLines = [
|
const expectedResponseLines = [
|
||||||
...commonResponseLines[responseFilename],
|
...commonResponseLines[responseFilename],
|
||||||
...[
|
...[
|
||||||
|
`[Warn] ${hr}`,
|
||||||
'[Warn] The following .dockerignore file(s) will not be used:',
|
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||||
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
`[Warn] * ${path.join(projectPath, 'src', '.dockerignore')}`,
|
||||||
'[Warn] Only one .dockerignore file at the source folder (project root) is used.',
|
'[Warn] By default, only one .dockerignore file at the source folder (project root)',
|
||||||
'[Warn] Additional .dockerignore files are disregarded. Microservices (multicontainer)',
|
'[Warn] is used. Microservices (multicontainer) applications may use a separate',
|
||||||
'[Warn] apps should place the .dockerignore file alongside the docker-compose.yml file.',
|
'[Warn] .dockerignore file for each service with the --multi-dockerignore (-m) option.',
|
||||||
'[Warn] See issue: https://github.com/balena-io/balena-cli/issues/1870',
|
'[Warn] See "balena help push" for more details.',
|
||||||
'[Warn] See also CLI v12 release notes: https://git.io/Jf7hz',
|
`[Warn] ${hr}`,
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
@ -291,6 +294,7 @@ describe('balena push', function () {
|
|||||||
);
|
);
|
||||||
const expectedResponseLines = [
|
const expectedResponseLines = [
|
||||||
...[
|
...[
|
||||||
|
`[Warn] ${hr}`,
|
||||||
'[Warn] Using file ignore patterns from:',
|
'[Warn] Using file ignore patterns from:',
|
||||||
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
|
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
|
||||||
`[Warn] * ${path.join(projectPath, '.gitignore')}`,
|
`[Warn] * ${path.join(projectPath, '.gitignore')}`,
|
||||||
@ -298,6 +302,7 @@ describe('balena push', function () {
|
|||||||
'[Warn] .gitignore files are being considered because the --gitignore option was used.',
|
'[Warn] .gitignore files are being considered because the --gitignore option was used.',
|
||||||
'[Warn] This option is deprecated and will be removed in the next major version release.',
|
'[Warn] This option is deprecated and will be removed in the next major version release.',
|
||||||
"[Warn] For more information, see 'balena help push'.",
|
"[Warn] For more information, see 'balena help push'.",
|
||||||
|
`[Warn] ${hr}`,
|
||||||
],
|
],
|
||||||
...commonResponseLines[responseFilename],
|
...commonResponseLines[responseFilename],
|
||||||
];
|
];
|
||||||
@ -364,45 +369,19 @@ describe('balena push', function () {
|
|||||||
'dockerignore2',
|
'dockerignore2',
|
||||||
);
|
);
|
||||||
const expectedFiles: ExpectedTarStreamFiles = {
|
const expectedFiles: ExpectedTarStreamFiles = {
|
||||||
'.dockerignore': { fileSize: 34, type: 'file' },
|
'.dockerignore': { fileSize: 33, type: 'file' },
|
||||||
'b.txt': { fileSize: 1, type: 'file' },
|
'b.txt': { fileSize: 1, type: 'file' },
|
||||||
Dockerfile: { fileSize: 13, type: 'file' },
|
Dockerfile: { fileSize: 13, type: 'file' },
|
||||||
|
'lib/.dockerignore': { fileSize: 10, type: 'file' },
|
||||||
|
'lib/src-b.txt': { fileSize: 5, type: 'file' },
|
||||||
'src/src-b.txt': { fileSize: 5, type: 'file' },
|
'src/src-b.txt': { fileSize: 5, type: 'file' },
|
||||||
'symlink-a.txt': { fileSize: 5, type: 'file' },
|
'symlink-a.txt': { fileSize: 5, type: 'file' },
|
||||||
...(isWindows ? { 'src/src-a.txt': { fileSize: 5, type: 'file' } } : {}),
|
...(isWindows
|
||||||
};
|
? {
|
||||||
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
|
'lib/src-a.txt': { fileSize: 5, type: 'file' },
|
||||||
const responseFilename = 'build-POST-v3.json';
|
'src/src-a.txt': { fileSize: 5, type: 'file' },
|
||||||
const responseBody = await fs.readFile(
|
}
|
||||||
path.join(builderResponsePath, responseFilename),
|
: {}),
|
||||||
'utf8',
|
|
||||||
);
|
|
||||||
|
|
||||||
await testPushBuildStream({
|
|
||||||
builderMock: builder,
|
|
||||||
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -l --gitignore`,
|
|
||||||
expectedFiles,
|
|
||||||
expectedQueryParams: commonQueryParams,
|
|
||||||
expectedResponseLines: commonResponseLines[responseFilename],
|
|
||||||
projectPath,
|
|
||||||
responseBody,
|
|
||||||
responseCode: 200,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create the expected tar stream (single container, dockerignore warn)', async () => {
|
|
||||||
const projectPath = path.join(
|
|
||||||
projectsPath,
|
|
||||||
'no-docker-compose',
|
|
||||||
'dockerignore2',
|
|
||||||
);
|
|
||||||
const expectedFiles: ExpectedTarStreamFiles = {
|
|
||||||
'.dockerignore': { fileSize: 34, type: 'file' },
|
|
||||||
'b.txt': { fileSize: 1, type: 'file' },
|
|
||||||
Dockerfile: { fileSize: 13, type: 'file' },
|
|
||||||
'src/src-b.txt': { fileSize: 5, type: 'file' },
|
|
||||||
'symlink-a.txt': { fileSize: 5, type: 'file' },
|
|
||||||
...(isWindows ? { 'src/src-a.txt': { fileSize: 5, type: 'file' } } : {}),
|
|
||||||
};
|
};
|
||||||
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
|
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
|
||||||
const responseFilename = 'build-POST-v3.json';
|
const responseFilename = 'build-POST-v3.json';
|
||||||
@ -412,6 +391,7 @@ describe('balena push', function () {
|
|||||||
);
|
);
|
||||||
const expectedResponseLines = isWindows
|
const expectedResponseLines = isWindows
|
||||||
? [
|
? [
|
||||||
|
`[Warn] ${hr}`,
|
||||||
'[Warn] Using file ignore patterns from:',
|
'[Warn] Using file ignore patterns from:',
|
||||||
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
|
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
|
||||||
'[Warn] The --gitignore option was used, but no .gitignore files were found.',
|
'[Warn] The --gitignore option was used, but no .gitignore files were found.',
|
||||||
@ -419,13 +399,61 @@ describe('balena push', function () {
|
|||||||
'[Warn] version release. It prevents the use of a better dockerignore parser and',
|
'[Warn] version release. It prevents the use of a better dockerignore parser and',
|
||||||
'[Warn] filter library that fixes several issues on Windows and improves compatibility',
|
'[Warn] filter library that fixes several issues on Windows and improves compatibility',
|
||||||
"[Warn] with 'docker build'. For more information, see 'balena help push'.",
|
"[Warn] with 'docker build'. For more information, see 'balena help push'.",
|
||||||
|
`[Warn] ${hr}`,
|
||||||
...commonResponseLines[responseFilename],
|
...commonResponseLines[responseFilename],
|
||||||
]
|
]
|
||||||
: commonResponseLines[responseFilename];
|
: commonResponseLines[responseFilename];
|
||||||
|
|
||||||
await testPushBuildStream({
|
await testPushBuildStream({
|
||||||
builderMock: builder,
|
builderMock: builder,
|
||||||
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -l -g`,
|
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -l --gitignore`,
|
||||||
|
expectedFiles,
|
||||||
|
expectedQueryParams: commonQueryParams,
|
||||||
|
expectedResponseLines,
|
||||||
|
projectPath,
|
||||||
|
responseBody,
|
||||||
|
responseCode: 200,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the expected tar stream (single container, --multi-dockerignore)', async () => {
|
||||||
|
const projectPath = path.join(
|
||||||
|
projectsPath,
|
||||||
|
'no-docker-compose',
|
||||||
|
'dockerignore2',
|
||||||
|
);
|
||||||
|
const expectedFiles: ExpectedTarStreamFiles = {
|
||||||
|
'.dockerignore': { fileSize: 33, type: 'file' },
|
||||||
|
'b.txt': { fileSize: 1, type: 'file' },
|
||||||
|
Dockerfile: { fileSize: 13, type: 'file' },
|
||||||
|
'lib/.dockerignore': { fileSize: 10, type: 'file' },
|
||||||
|
'lib/src-b.txt': { fileSize: 5, type: 'file' },
|
||||||
|
'src/src-b.txt': { fileSize: 5, type: 'file' },
|
||||||
|
'symlink-a.txt': { fileSize: 5, type: 'file' },
|
||||||
|
};
|
||||||
|
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
|
||||||
|
const responseFilename = 'build-POST-v3.json';
|
||||||
|
const responseBody = await fs.readFile(
|
||||||
|
path.join(builderResponsePath, responseFilename),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const expectedResponseLines: string[] = [
|
||||||
|
...[
|
||||||
|
`[Warn] ${hr}`,
|
||||||
|
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||||
|
`[Warn] * ${path.join(projectPath, 'lib', '.dockerignore')}`,
|
||||||
|
'[Warn] When --multi-dockerignore (-m) is used, only .dockerignore files at the root of',
|
||||||
|
"[Warn] each service's build context (in a microservices/multicontainer application),",
|
||||||
|
'[Warn] plus a .dockerignore file at the overall project root, are used.',
|
||||||
|
'[Warn] See "balena help push" for more details.',
|
||||||
|
`[Warn] ${hr}`,
|
||||||
|
],
|
||||||
|
...commonResponseLines[responseFilename],
|
||||||
|
];
|
||||||
|
|
||||||
|
await testPushBuildStream({
|
||||||
|
builderMock: builder,
|
||||||
|
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -l -m`,
|
||||||
expectedFiles,
|
expectedFiles,
|
||||||
expectedQueryParams: commonQueryParams,
|
expectedQueryParams: commonQueryParams,
|
||||||
expectedResponseLines,
|
expectedResponseLines,
|
||||||
@ -444,12 +472,13 @@ describe('balena push', function () {
|
|||||||
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
|
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
|
||||||
'service1/file1.sh': { fileSize: 12, type: 'file' },
|
'service1/file1.sh': { fileSize: 12, type: 'file' },
|
||||||
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' },
|
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||||
'service2/.dockerignore': { fileSize: 14, type: 'file' },
|
'service2/.dockerignore': { fileSize: 12, type: 'file' },
|
||||||
'service2/file2-crlf.sh': {
|
'service2/file2-crlf.sh': {
|
||||||
fileSize: isWindows ? 12 : 14,
|
fileSize: isWindows ? 12 : 14,
|
||||||
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
},
|
},
|
||||||
|
'service2/src/file1.sh': { fileSize: 12, type: 'file' },
|
||||||
};
|
};
|
||||||
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
|
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
|
||||||
const responseFilename = 'build-POST-v3.json';
|
const responseFilename = 'build-POST-v3.json';
|
||||||
@ -460,14 +489,14 @@ describe('balena push', function () {
|
|||||||
const expectedResponseLines: string[] = [
|
const expectedResponseLines: string[] = [
|
||||||
...commonResponseLines[responseFilename],
|
...commonResponseLines[responseFilename],
|
||||||
...[
|
...[
|
||||||
|
`[Warn] ${hr}`,
|
||||||
'[Warn] The following .dockerignore file(s) will not be used:',
|
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||||
`[Warn] * ${path.join(projectPath, 'service2', '.dockerignore')}`,
|
`[Warn] * ${path.join(projectPath, 'service2', '.dockerignore')}`,
|
||||||
'[Warn] Only one .dockerignore file at the source folder (project root) is used.',
|
'[Warn] By default, only one .dockerignore file at the source folder (project root)',
|
||||||
'[Warn] Additional .dockerignore files are disregarded. Microservices (multicontainer)',
|
'[Warn] is used. Microservices (multicontainer) applications may use a separate',
|
||||||
'[Warn] apps should place the .dockerignore file alongside the docker-compose.yml file.',
|
'[Warn] .dockerignore file for each service with the --multi-dockerignore (-m) option.',
|
||||||
'[Warn] See issue: https://github.com/balena-io/balena-cli/issues/1870',
|
'[Warn] See "balena help push" for more details.',
|
||||||
'[Warn] See also CLI v12 release notes: https://git.io/Jf7hz',
|
`[Warn] ${hr}`,
|
||||||
'[Warn] -------------------------------------------------------------------------------',
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
@ -491,6 +520,61 @@ describe('balena push', function () {
|
|||||||
responseCode: 200,
|
responseCode: 200,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create the expected tar stream (docker-compose, --multi-dockerignore)', async () => {
|
||||||
|
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
|
||||||
|
const expectedFiles: ExpectedTarStreamFiles = {
|
||||||
|
'.balena/balena.yml': { fileSize: 197, type: 'file' },
|
||||||
|
'.dockerignore': { fileSize: 22, type: 'file' },
|
||||||
|
'docker-compose.yml': { fileSize: 245, type: 'file' },
|
||||||
|
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
|
||||||
|
'service1/file1.sh': { fileSize: 12, type: 'file' },
|
||||||
|
'service1/test-ignore.txt': { fileSize: 12, type: 'file' },
|
||||||
|
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||||
|
'service2/.dockerignore': { fileSize: 12, type: 'file' },
|
||||||
|
'service2/file2-crlf.sh': {
|
||||||
|
fileSize: isWindows ? 12 : 14,
|
||||||
|
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
||||||
|
type: 'file',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
|
||||||
|
const responseFilename = 'build-POST-v3.json';
|
||||||
|
const responseBody = await fs.readFile(
|
||||||
|
path.join(builderResponsePath, responseFilename),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const expectedResponseLines: string[] = [
|
||||||
|
...commonResponseLines[responseFilename],
|
||||||
|
...[
|
||||||
|
`[Info] ${hr}`,
|
||||||
|
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
|
||||||
|
'[Info] found at the project source (root) directory. Note that this file will not',
|
||||||
|
'[Info] be used to filter service subdirectories. See "balena help push".',
|
||||||
|
`[Info] ${hr}`,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if (isWindows) {
|
||||||
|
expectedResponseLines.push(
|
||||||
|
`[Info] Converting line endings CRLF -> LF for file: ${path.join(
|
||||||
|
projectPath,
|
||||||
|
'service2',
|
||||||
|
'file2-crlf.sh',
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await testPushBuildStream({
|
||||||
|
builderMock: builder,
|
||||||
|
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -l -m`,
|
||||||
|
expectedFiles,
|
||||||
|
expectedQueryParams: commonQueryParams,
|
||||||
|
expectedResponseLines,
|
||||||
|
projectPath,
|
||||||
|
responseBody,
|
||||||
|
responseCode: 200,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('balena push: project validation', function () {
|
describe('balena push: project validation', function () {
|
||||||
|
@ -95,7 +95,11 @@ export async function inspectTarStream(
|
|||||||
expect($expected).to.deep.equal(found);
|
expect($expected).to.deep.equal(found);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { diff } = require('deep-object-diff');
|
const { diff } = require('deep-object-diff');
|
||||||
const diffStr = JSON.stringify(diff($expected, found), null, 4);
|
const diffStr = JSON.stringify(
|
||||||
|
diff($expected, found),
|
||||||
|
(_k, v) => (v === undefined ? 'undefined' : v),
|
||||||
|
4,
|
||||||
|
);
|
||||||
console.error(`\nexpected vs. found diff:\n${diffStr}\n`);
|
console.error(`\nexpected vs. found diff:\n${diffStr}\n`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -181,7 +185,9 @@ export async function testDockerBuildStream(o: {
|
|||||||
inspectTarStream(buildRequestBody, expectedFiles, projectPath),
|
inspectTarStream(buildRequestBody, expectedFiles, projectPath),
|
||||||
tag,
|
tag,
|
||||||
});
|
});
|
||||||
o.dockerMock.expectGetImages();
|
if (o.commandLine.startsWith('build')) {
|
||||||
|
o.dockerMock.expectGetImages();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { exitCode, out, err } = await runCommand(o.commandLine);
|
const { exitCode, out, err } = await runCommand(o.commandLine);
|
||||||
|
@ -1 +1 @@
|
|||||||
file2-crlf.sh
|
**/file1.sh
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
line1
|
||||||
|
line2
|
@ -1,3 +1,3 @@
|
|||||||
a.txt
|
a.txt
|
||||||
src/src-a.txt
|
**/src-a.txt
|
||||||
symlink-b.txt
|
symlink-b.txt
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
src-b.txt
|
@ -0,0 +1 @@
|
|||||||
|
lib-a
|
@ -0,0 +1 @@
|
|||||||
|
lib-b
|
@ -99,9 +99,11 @@ describe('compare new and old tarDirectory implementations', function () {
|
|||||||
'dockerignore2',
|
'dockerignore2',
|
||||||
);
|
);
|
||||||
const expectedFiles = {
|
const expectedFiles = {
|
||||||
'.dockerignore': { fileSize: 34, type: 'file' },
|
'.dockerignore': { fileSize: 33, type: 'file' },
|
||||||
'b.txt': { fileSize: 1, type: 'file' },
|
'b.txt': { fileSize: 1, type: 'file' },
|
||||||
Dockerfile: { fileSize: 13, type: 'file' },
|
Dockerfile: { fileSize: 13, type: 'file' },
|
||||||
|
'lib/.dockerignore': { fileSize: 10, type: 'file' },
|
||||||
|
'lib/src-b.txt': { fileSize: 5, type: 'file' },
|
||||||
'src/src-b.txt': { fileSize: 5, type: 'file' },
|
'src/src-b.txt': { fileSize: 5, type: 'file' },
|
||||||
'symlink-a.txt': { fileSize: 5, type: 'file' },
|
'symlink-a.txt': { fileSize: 5, type: 'file' },
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user