Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
52af76b7ed | |||
414d7dd727 | |||
4b1c908b89 | |||
6e313261e7 | |||
42df15a523 | |||
7ef2ebcf5b | |||
87f26b7b63 | |||
ffdc61fb42 | |||
5b25704cf8 | |||
00e6033d8b | |||
453040983d | |||
456858bc36 | |||
606c82dafa | |||
9fc4afe4b8 | |||
90689c2645 | |||
4a1d649e5e | |||
66bd36dc4f | |||
349d711cc5 | |||
c74a4bcd5b | |||
ff835c4160 | |||
05eceeb056 | |||
de55beda08 | |||
9aa2b0c7be | |||
a283bbfba0 | |||
8a4e300581 | |||
6b0d96b745 | |||
a08c11838f | |||
7c5225125c | |||
bc4778f7db | |||
2e7e8ae5cf | |||
054585c7f5 | |||
c0cebbe614 |
@@ -6,8 +6,8 @@ on:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
pnpm install -g @ship.zone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
@@ -6,8 +6,8 @@ on:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Test stable
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Release
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Code quality
|
||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,7 +3,6 @@
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
pages/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
@@ -17,4 +16,8 @@ node_modules/
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
# custom
|
||||
# AI
|
||||
.claude/
|
||||
.serena/
|
||||
|
||||
#------# custom
|
BIN
.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl
vendored
Normal file
BIN
.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl
vendored
Normal file
Binary file not shown.
68
.serena/project.yml
Normal file
68
.serena/project.yml
Normal file
@@ -0,0 +1,68 @@
|
||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||
# * For C, use cpp
|
||||
# * For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||
language: typescript
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "docker"
|
239
changelog.md
Normal file
239
changelog.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-08-19 - 1.3.5 - fix(core)
|
||||
Stabilize CI/workflows and runtime: update CI images/metadata, improve streaming requests and image handling, and fix tests & package metadata
|
||||
|
||||
- Update CI workflows and images: switch workflow IMAGE to code.foss.global/host.today/ht-docker-node:npmci, fix NPMCI_COMPUTED_REPOURL placeholders, and replace @shipzone/npmci with @ship.zone/npmci in workflows
|
||||
- Update npmextra.json gitzone metadata (githost -> code.foss.global, gitscope -> apiclient.xyz, npmPackagename -> @apiclient.xyz/docker) and npmdocker.baseImage -> host.today/ht-docker-node:npmci
|
||||
- Adjust package.json repository/bugs/homepage to code.foss.global, add pnpm overrides entry and normalize package metadata
|
||||
- Improve DockerHost streaming and request handling: reduce requestStreaming timeout to 30s, enable autoDrain for streaming requests, improve response parsing for streaming vs JSON endpoints to avoid hangs
|
||||
- Enhance DockerImage and DockerImageStore stream handling and tar processing: more robust import/export parsing, safer stream-to-file writes, repackaging steps, and error handling
|
||||
- Unskip and update tests: re-enable DockerImageStore integration test, change stored image name to 'hello2', add formatting fixes and ensure cleanup stops the test DockerHost
|
||||
- Miscellaneous code and docs cleanup: numerous formatting fixes and trailing-comma normalization across README and TS sources, update commitinfo and logger newline fixes, and add local tool ignores (.claude/.serena) to .gitignore
|
||||
|
||||
## 2025-08-19 - 1.3.4 - fix(test)
|
||||
|
||||
Increase test timeout, enable DockerImageStore test, update test image name, bump smartrequest patch, and add local claude settings
|
||||
|
||||
- Increase tstest timeout from 120s to 600s in package.json to accommodate longer-running integration tests.
|
||||
- Unskip the DockerImageStore integration test and change stored image name from 'hello' to 'hello2' in test/test.nonci.node.ts.
|
||||
- Bump dependency @push.rocks/smartrequest from ^4.3.0 to ^4.3.1.
|
||||
- Add .claude/settings.local.json to allow local agent permissions for running tests and related tooling.
|
||||
|
||||
## 2025-08-19 - 1.3.3 - fix(classes.host)
|
||||
|
||||
Adjust requestStreaming timeout and autoDrain; stabilize tests
|
||||
|
||||
- Reduced requestStreaming timeout from 10 minutes to 30 seconds to avoid long-running hanging requests.
|
||||
- Enabled autoDrain for streaming requests to ensure response streams are properly drained and reduce resource issues.
|
||||
- Marked the DockerImageStore S3 integration test as skipped to avoid CI dependence on external S3 and added a cleanup test to stop the test DockerHost.
|
||||
- Added local tool settings file (.claude/settings.local.json) with local permissions (development-only).
|
||||
|
||||
## 2025-08-18 - 1.3.2 - fix(package.json)
|
||||
|
||||
Fix test script timeout typo, update dependency versions, and add typings & project configs
|
||||
|
||||
- Fix test script: correct 'tineout' -> 'timeout' for npm test command and set timeout to 120s
|
||||
- Add 'typings': 'dist_ts/index.d.ts' to package.json
|
||||
- Bump dependencies to newer compatible versions (notable packages: @push.rocks/lik, @push.rocks/smartarchive, @push.rocks/smartbucket, @push.rocks/smartfile, @push.rocks/smartlog, @push.rocks/smartpromise, @push.rocks/smartstream, rxjs)
|
||||
- Add project/config files: .serena/project.yml and .claude/settings.local.json (editor/CI metadata)
|
||||
- Include generated cache/metadata files (typescript document symbols cache) — not source changes but tooling/cache artifacts
|
||||
|
||||
## 2025-08-18 - 1.3.1 - fix(test)
|
||||
|
||||
Update test setup and devDependencies; adjust test import and add package metadata
|
||||
|
||||
- Update test script to run with additional flags: --verbose, --logfile and --tineout 120
|
||||
- Bump devDependencies: @git.zone/tsbuild -> ^2.6.7, @git.zone/tsrun -> ^1.3.3, @git.zone/tstest -> ^2.3.5, @push.rocks/qenv -> ^6.1.3
|
||||
- Change test import from @push.rocks/tapbundle to @git.zone/tstest/tapbundle
|
||||
- Add typings field (dist_ts/index.d.ts)
|
||||
- Add packageManager field for pnpm@10.14.0 with integrity hash
|
||||
|
||||
## 2024-12-23 - 1.3.0 - feat(core)
|
||||
|
||||
Initial release of Docker client with TypeScript support
|
||||
|
||||
- Provides easy communication with Docker's remote API from Node.js
|
||||
- Includes implementations for managing Docker services, networks, secrets, containers, and images
|
||||
|
||||
## 2024-12-23 - 1.2.8 - fix(core)
|
||||
|
||||
Improved the image creation process from tar stream in DockerImage class.
|
||||
|
||||
- Enhanced `DockerImage.createFromTarStream` method to handle streamed response and parse imported image details.
|
||||
- Fixed the dependency version for `@push.rocks/smartarchive` in package.json.
|
||||
|
||||
## 2024-10-13 - 1.2.7 - fix(core)
|
||||
|
||||
Prepare patch release with minor fixes and improvements
|
||||
|
||||
## 2024-10-13 - 1.2.6 - fix(core)
|
||||
|
||||
Minor refactoring and code quality improvements.
|
||||
|
||||
## 2024-10-13 - 1.2.5 - fix(dependencies)
|
||||
|
||||
Update dependencies for stability improvements
|
||||
|
||||
- Updated @push.rocks/smartstream to version ^3.0.46
|
||||
- Updated @push.rocks/tapbundle to version ^5.3.0
|
||||
- Updated @types/node to version 22.7.5
|
||||
|
||||
## 2024-10-13 - 1.2.4 - fix(core)
|
||||
|
||||
Refactored DockerImageStore constructor to remove DockerHost dependency
|
||||
|
||||
- Adjusted DockerImageStore constructor to remove dependency on DockerHost
|
||||
- Updated ts/classes.host.ts to align with DockerImageStore's new constructor signature
|
||||
|
||||
## 2024-08-21 - 1.2.3 - fix(dependencies)
|
||||
|
||||
Update dependencies to the latest versions and fix image export test
|
||||
|
||||
- Updated several dependencies to their latest versions in package.json.
|
||||
- Enabled the previously skipped 'should export images' test.
|
||||
|
||||
## 2024-06-10 - 1.2.1-1.2.2 - Core/General
|
||||
|
||||
General updates and fixes.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2024-06-10 - 1.2.0 - Core
|
||||
|
||||
Core updates and bug fixes.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2024-06-08 - 1.2.0 - General/Core
|
||||
|
||||
Major release with core enhancements.
|
||||
|
||||
- Processing images with extraction, retagging, repackaging, and long-term storage
|
||||
|
||||
## 2024-06-06 - 1.1.4 - General/Imagestore
|
||||
|
||||
Significant feature addition.
|
||||
|
||||
- Add feature to process images with extraction, retagging, repackaging, and long-term storage
|
||||
|
||||
## 2024-05-08 - 1.0.112 - Images
|
||||
|
||||
Add new functionality for image handling.
|
||||
|
||||
- Can now import and export images
|
||||
- Start work on local 100% JS OCI image registry
|
||||
|
||||
## 2024-06-05 - 1.1.0-1.1.3 - Core
|
||||
|
||||
Regular updates and fixes.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2024-02-02 - 1.0.105-1.0.110 - Core
|
||||
|
||||
Routine core updates and fixes.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2022-10-17 - 1.0.103-1.0.104 - Core
|
||||
|
||||
Routine core updates.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2020-10-01 - 1.0.99-1.0.102 - Core
|
||||
|
||||
Routine core updates.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2019-09-22 - 1.0.73-1.0.78 - Core
|
||||
|
||||
Routine updates and core fixes.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2019-09-13 - 1.0.60-1.0.72 - Core
|
||||
|
||||
Routine updates and core fixes.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2019-08-16 - 1.0.43-1.0.59 - Core
|
||||
|
||||
Routine updates and core fixes.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2019-08-15 - 1.0.37-1.0.42 - Core
|
||||
|
||||
Routine updates and core fixes.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2019-08-14 - 1.0.31-1.0.36 - Core
|
||||
|
||||
Routine updates and core fixes.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2019-01-10 - 1.0.27-1.0.30 - Core
|
||||
|
||||
Routine updates and core fixes.
|
||||
|
||||
- Fix core update
|
||||
|
||||
## 2018-07-16 - 1.0.23-1.0.24 - Core
|
||||
|
||||
Routine updates and core fixes.
|
||||
|
||||
- Fix core shift to new style
|
||||
|
||||
## 2017-07-16 - 1.0.20-1.0.22 - General
|
||||
|
||||
Routine updates and fixes.
|
||||
|
||||
- Update node_modules within npmdocker
|
||||
|
||||
## 2017-04-02 - 1.0.18-1.0.19 - General
|
||||
|
||||
Routine updates and fixes.
|
||||
|
||||
- Work with npmdocker and npmts 7.x.x
|
||||
- CI updates
|
||||
|
||||
## 2016-07-31 - 1.0.17 - General
|
||||
|
||||
Enhancements and fixes.
|
||||
|
||||
- Now waiting for response to be stored before ending streaming request
|
||||
- Cosmetic fix
|
||||
|
||||
## 2016-07-29 - 1.0.14-1.0.16 - General
|
||||
|
||||
Multiple updates and features added.
|
||||
|
||||
- Fix request for change observable and add npmdocker
|
||||
- Add request typings
|
||||
|
||||
## 2016-07-28 - 1.0.13 - Core
|
||||
|
||||
Fixes and preparations.
|
||||
|
||||
- Fixed request for newer docker
|
||||
- Prepare for npmdocker
|
||||
|
||||
## 2016-06-16 - 1.0.0-1.0.2 - General
|
||||
|
||||
Initial sequence of releases, significant feature additions and CI setups.
|
||||
|
||||
- Implement container start and stop
|
||||
- Implement list containers and related functions
|
||||
- Add tests with in docker environment
|
||||
|
||||
## 2016-04-12 - unknown - Initial Commit
|
||||
|
||||
Initial project setup.
|
||||
|
||||
- Initial commit
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"npmdocker": {
|
||||
"baseImage": "hosttoday/ht-docker-node:npmci",
|
||||
"baseImage": "host.today/ht-docker-node:npmci",
|
||||
"command": "(ls -a && rm -r node_modules && yarn global add npmts && yarn install && npmts)",
|
||||
"dockerSock": true
|
||||
},
|
||||
@@ -12,11 +12,11 @@
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"gitscope": "mojoio",
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "apiclient.xyz",
|
||||
"gitrepo": "docker",
|
||||
"description": "Provides easy communication with Docker remote API from Node.js, with TypeScript support.",
|
||||
"npmPackagename": "@mojoio/docker",
|
||||
"npmPackagename": "@apiclient.xyz/docker",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"Docker",
|
||||
|
51
package.json
51
package.json
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@apiclient.xyz/docker",
|
||||
"version": "1.1.4",
|
||||
"version": "1.3.5",
|
||||
"description": "Provides easy communication with Docker remote API from Node.js, with TypeScript support.",
|
||||
"private": false,
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --web)",
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 600)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://gitlab.com/mojoio/docker.git"
|
||||
"url": "https://code.foss.global/apiclient.xyz/docker.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Docker",
|
||||
@@ -29,32 +29,33 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/mojoio/docker/issues"
|
||||
"url": "https://code.foss.global/apiclient.xyz/docker/issues"
|
||||
},
|
||||
"homepage": "https://gitlab.com/mojoio/docker#readme",
|
||||
"homepage": "https://code.foss.global/apiclient.xyz/docker#readme",
|
||||
"dependencies": {
|
||||
"@push.rocks/lik": "^6.0.15",
|
||||
"@push.rocks/smartarchive": "^4.0.22",
|
||||
"@push.rocks/smartbucket": "^3.0.10",
|
||||
"@push.rocks/smartfile": "^11.0.16",
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartarchive": "^4.2.2",
|
||||
"@push.rocks/smartbucket": "^3.3.10",
|
||||
"@push.rocks/smartfile": "^11.2.7",
|
||||
"@push.rocks/smartjson": "^5.0.20",
|
||||
"@push.rocks/smartlog": "^3.0.6",
|
||||
"@push.rocks/smartnetwork": "^3.0.0",
|
||||
"@push.rocks/smartpath": "^5.0.18",
|
||||
"@push.rocks/smartpromise": "^4.0.3",
|
||||
"@push.rocks/smartrequest": "^2.0.22",
|
||||
"@push.rocks/smartstream": "^3.0.44",
|
||||
"@push.rocks/smartlog": "^3.1.8",
|
||||
"@push.rocks/smartnetwork": "^4.1.2",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^4.3.1",
|
||||
"@push.rocks/smartstream": "^3.2.5",
|
||||
"@push.rocks/smartstring": "^4.0.15",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@push.rocks/smartversion": "^3.0.5",
|
||||
"@tsclass/tsclass": "^4.0.54",
|
||||
"rxjs": "^7.5.7"
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
"rxjs": "^7.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.80",
|
||||
"@git.zone/tsrun": "^1.2.12",
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@push.rocks/tapbundle": "^5.0.23",
|
||||
"@types/node": "20.14.1"
|
||||
"@git.zone/tsbuild": "^2.6.7",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.5",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@types/node": "22.7.5"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -70,5 +71,9 @@
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
]
|
||||
],
|
||||
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
|
||||
"pnpm": {
|
||||
"overrides": {}
|
||||
}
|
||||
}
|
||||
|
7676
pnpm-lock.yaml
generated
7676
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
qenv.yml
Normal file
6
qenv.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
required:
|
||||
- S3_ENDPOINT
|
||||
- S3_ACCESSKEY
|
||||
- S3_ACCESSSECRET
|
||||
- S3_BUCKET
|
||||
|
536
readme.md
536
readme.md
@@ -1,136 +1,504 @@
|
||||
# @apiclient.xyz/docker
|
||||
# @apiclient.xyz/docker 🐳
|
||||
|
||||
easy communication with docker remote api from node, TypeScript ready
|
||||
> **Powerful TypeScript client for Docker Remote API** - Build, manage, and orchestrate Docker containers, images, networks, and more with type-safe elegance.
|
||||
|
||||
## Install
|
||||
## 🚀 Features
|
||||
|
||||
To install @apiclient.xyz/docker, you can use npm (npm package manager). Run the following command in your terminal:
|
||||
- 🎯 **Full TypeScript Support** - Complete type definitions for Docker API entities
|
||||
- 🔄 **Async/Await Ready** - Modern promise-based architecture for seamless async operations
|
||||
- 📦 **Container Management** - Create, list, inspect, and remove containers effortlessly
|
||||
- 🖼️ **Image Handling** - Pull from registries, build from tarballs, export, and manage tags
|
||||
- 🌐 **Network Operations** - Create and manage Docker networks with full IPAM support
|
||||
- 🔐 **Secrets Management** - Handle Docker secrets securely in swarm mode
|
||||
- 🎭 **Service Orchestration** - Deploy and manage services in Docker Swarm
|
||||
- 💾 **S3 Image Storage** - Built-in support for storing/retrieving images from S3
|
||||
- 📊 **Event Streaming** - Real-time Docker event monitoring with RxJS observables
|
||||
- 🔧 **Registry Authentication** - Seamless authentication with Docker registries
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
# Using npm
|
||||
npm install @apiclient.xyz/docker --save
|
||||
|
||||
# Using pnpm (recommended)
|
||||
pnpm add @apiclient.xyz/docker
|
||||
|
||||
# Using yarn
|
||||
yarn add @apiclient.xyz/docker
|
||||
```
|
||||
|
||||
This command installs the package and adds it as a dependency to your project's `package.json` file.
|
||||
|
||||
## Usage
|
||||
|
||||
The `@apiclient.xyz/docker` package provides a TypeScript-ready interface for interacting with Docker's Remote API directly from Node.js applications. It leverages TypeScript for strong type definitions, ensuring more reliable and maintainable code.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, ensure:
|
||||
|
||||
- You have Docker installed and running on your machine or a remote server.
|
||||
- You are familiar with TypeScript and have it set up in your development environment.
|
||||
|
||||
### Getting Started
|
||||
|
||||
First, import the required classes from the package:
|
||||
## 🎯 Quick Start
|
||||
|
||||
```typescript
|
||||
import { DockerHost, DockerContainer, DockerService, DockerNetwork } from '@apiclient.xyz/docker';
|
||||
import { DockerHost } from '@apiclient.xyz/docker';
|
||||
|
||||
// Connect to local Docker daemon
|
||||
const docker = new DockerHost();
|
||||
|
||||
// Or connect to remote Docker host
|
||||
const remoteDocker = new DockerHost({
|
||||
socketPath: 'tcp://remote-docker-host:2375',
|
||||
});
|
||||
```
|
||||
|
||||
### Instantiate DockerHost
|
||||
## 📚 Complete API Guide
|
||||
|
||||
Start by creating a `DockerHost` instance. This class is the entry point to communicate with the Docker Remote API.
|
||||
### 🐳 DockerHost - Your Gateway to Docker
|
||||
|
||||
The `DockerHost` class is your primary interface to interact with the Docker daemon.
|
||||
|
||||
```typescript
|
||||
// Connect to local Docker instance
|
||||
const localDockerHost = new DockerHost();
|
||||
import { DockerHost } from '@apiclient.xyz/docker';
|
||||
|
||||
// Or specify a custom path or URL to a Docker host
|
||||
const remoteDockerHost = new DockerHost('tcp://<REMOTE_DOCKER_HOST>:2375');
|
||||
// Initialize with default local socket
|
||||
const docker = new DockerHost();
|
||||
|
||||
// Custom initialization options
|
||||
const customDocker = new DockerHost({
|
||||
socketPath: '/var/run/docker.sock', // Unix socket path
|
||||
// or
|
||||
socketPath: 'tcp://192.168.1.100:2375', // TCP connection
|
||||
});
|
||||
|
||||
// Start and stop (for lifecycle management)
|
||||
await docker.start();
|
||||
// ... do your work
|
||||
await docker.stop();
|
||||
```
|
||||
|
||||
### Working with Containers
|
||||
### 📦 Container Management
|
||||
|
||||
#### List All Containers
|
||||
|
||||
```typescript
|
||||
async function listAllContainers() {
|
||||
const containers = await localDockerHost.getContainers();
|
||||
console.log(containers);
|
||||
}
|
||||
// Get all containers (including stopped ones)
|
||||
const allContainers = await docker.getContainers();
|
||||
|
||||
listAllContainers();
|
||||
// Each container includes detailed information
|
||||
allContainers.forEach((container) => {
|
||||
console.log(`Container: ${container.Names[0]}`);
|
||||
console.log(` ID: ${container.Id}`);
|
||||
console.log(` Status: ${container.Status}`);
|
||||
console.log(` Image: ${container.Image}`);
|
||||
console.log(` State: ${container.State}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### Create and Remove a Container
|
||||
#### Create and Manage Containers
|
||||
|
||||
```typescript
|
||||
import { IContainerCreationDescriptor } from '@apiclient.xyz/docker';
|
||||
import { DockerContainer } from '@apiclient.xyz/docker';
|
||||
|
||||
async function createAndRemoveContainer() {
|
||||
const containerDescriptor: IContainerCreationDescriptor = {
|
||||
Hostname: 'test-container',
|
||||
Domainname: '',
|
||||
// Additional settings here
|
||||
};
|
||||
// Create a container with detailed configuration
|
||||
const container = await DockerContainer.create(docker, {
|
||||
Image: 'nginx:latest',
|
||||
name: 'my-nginx-server',
|
||||
HostConfig: {
|
||||
PortBindings: {
|
||||
'80/tcp': [{ HostPort: '8080' }],
|
||||
},
|
||||
RestartPolicy: {
|
||||
Name: 'unless-stopped',
|
||||
},
|
||||
Memory: 512 * 1024 * 1024, // 512MB memory limit
|
||||
},
|
||||
Env: ['NODE_ENV=production', 'LOG_LEVEL=info'],
|
||||
Labels: {
|
||||
app: 'web-server',
|
||||
environment: 'production',
|
||||
},
|
||||
});
|
||||
|
||||
// Create container
|
||||
const container = await DockerContainer.create(localDockerHost, containerDescriptor);
|
||||
console.log(`Container Created: ${container.Id}`);
|
||||
console.log(`Container created: ${container.Id}`);
|
||||
|
||||
// Remove container
|
||||
await container.remove();
|
||||
console.log(`Container Removed: ${container.Id}`);
|
||||
}
|
||||
|
||||
createAndRemoveContainer();
|
||||
// Container operations (these would need to be implemented)
|
||||
// await container.start();
|
||||
// await container.stop();
|
||||
// await container.remove();
|
||||
```
|
||||
|
||||
### Working with Docker Services
|
||||
|
||||
#### Create a Docker Service
|
||||
#### Get Container by ID
|
||||
|
||||
```typescript
|
||||
import { IServiceCreationDescriptor } from '@apiclient.xyz/docker';
|
||||
|
||||
async function createDockerService() {
|
||||
const serviceDescriptor: IServiceCreationDescriptor = {
|
||||
name: 'my-service',
|
||||
image: 'nginx:latest', // Docker Image
|
||||
// Additional settings
|
||||
};
|
||||
|
||||
const service = await DockerService.createService(localDockerHost, serviceDescriptor);
|
||||
console.log(`Service Created: ${service.Id}`);
|
||||
const container = await DockerContainer.getContainerById(
|
||||
docker,
|
||||
'container-id-here',
|
||||
);
|
||||
if (container) {
|
||||
console.log(`Found container: ${container.Names[0]}`);
|
||||
}
|
||||
|
||||
createDockerService();
|
||||
```
|
||||
|
||||
### Working with Docker Networks
|
||||
### 🖼️ Image Management
|
||||
|
||||
#### Listing and Creating Networks
|
||||
#### Pull Images from Registry
|
||||
|
||||
```typescript
|
||||
async function listAndCreateNetwork() {
|
||||
// List all networks
|
||||
const networks = await localDockerHost.getNetworks();
|
||||
console.log(networks);
|
||||
import { DockerImage } from '@apiclient.xyz/docker';
|
||||
|
||||
// Create a new network
|
||||
const network = await DockerNetwork.createNetwork(localDockerHost, {
|
||||
Name: 'my-network'
|
||||
// Additional settings
|
||||
// Pull an image from Docker Hub
|
||||
const image = await DockerImage.createFromRegistry(docker, {
|
||||
imageName: 'node',
|
||||
imageTag: '18-alpine',
|
||||
// Optional: provide registry authentication
|
||||
authToken: 'your-registry-auth-token',
|
||||
});
|
||||
|
||||
console.log(`Image pulled: ${image.RepoTags[0]}`);
|
||||
console.log(`Size: ${(image.Size / 1024 / 1024).toFixed(2)} MB`);
|
||||
```
|
||||
|
||||
#### Import Images from Tar
|
||||
|
||||
```typescript
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Import from a tar stream
|
||||
const tarStream = fs.createReadStream('./my-image.tar');
|
||||
const importedImage = await DockerImage.createFromTarStream(docker, {
|
||||
tarStream,
|
||||
imageUrl: 'file://./my-image.tar',
|
||||
imageTag: 'my-app:v1.0.0',
|
||||
});
|
||||
```
|
||||
|
||||
#### Export Images to Tar
|
||||
|
||||
```typescript
|
||||
// Export an image to a tar stream
|
||||
const image = await DockerImage.getImageByName(docker, 'nginx:latest');
|
||||
const exportStream = await image.exportToTarStream();
|
||||
|
||||
// Save to file
|
||||
const writeStream = fs.createWriteStream('./nginx-export.tar');
|
||||
exportStream.pipe(writeStream);
|
||||
```
|
||||
|
||||
#### Tag Images
|
||||
|
||||
```typescript
|
||||
// Tag an existing image
|
||||
await DockerImage.tagImageByIdOrName(docker, 'node:18-alpine', {
|
||||
registry: 'myregistry.com',
|
||||
imageName: 'my-node-app',
|
||||
imageTag: 'v2.0.0',
|
||||
});
|
||||
// Result: myregistry.com/my-node-app:v2.0.0
|
||||
```
|
||||
|
||||
### 🌐 Network Management
|
||||
|
||||
#### Create Custom Networks
|
||||
|
||||
```typescript
|
||||
import { DockerNetwork } from '@apiclient.xyz/docker';
|
||||
|
||||
// Create a bridge network
|
||||
const network = await DockerNetwork.createNetwork(docker, {
|
||||
Name: 'my-app-network',
|
||||
Driver: 'bridge',
|
||||
EnableIPv6: false,
|
||||
IPAM: {
|
||||
Driver: 'default',
|
||||
Config: [
|
||||
{
|
||||
Subnet: '172.28.0.0/16',
|
||||
Gateway: '172.28.0.1',
|
||||
},
|
||||
],
|
||||
},
|
||||
Labels: {
|
||||
project: 'my-app',
|
||||
environment: 'production',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Network created: ${network.Id}`);
|
||||
```
|
||||
|
||||
#### List and Inspect Networks
|
||||
|
||||
```typescript
|
||||
// Get all networks
|
||||
const networks = await docker.getNetworks();
|
||||
networks.forEach((net) => {
|
||||
console.log(`Network: ${net.Name} (${net.Driver})`);
|
||||
console.log(` Scope: ${net.Scope}`);
|
||||
console.log(` Internal: ${net.Internal}`);
|
||||
});
|
||||
|
||||
// Get specific network
|
||||
const appNetwork = await DockerNetwork.getNetworkByName(
|
||||
docker,
|
||||
'my-app-network',
|
||||
);
|
||||
|
||||
// Get containers on network
|
||||
const containers = await appNetwork.getContainersOnNetwork();
|
||||
console.log(`Containers on network: ${containers.length}`);
|
||||
```
|
||||
|
||||
### 🎭 Service Management (Swarm Mode)
|
||||
|
||||
#### Deploy Services
|
||||
|
||||
```typescript
|
||||
import { DockerService } from '@apiclient.xyz/docker';
|
||||
|
||||
// Create a replicated service
|
||||
const service = await DockerService.createService(docker, {
|
||||
name: 'web-api',
|
||||
image: 'my-api:latest',
|
||||
replicas: 3,
|
||||
ports: [
|
||||
{
|
||||
Protocol: 'tcp',
|
||||
PublishedPort: 80,
|
||||
TargetPort: 3000,
|
||||
},
|
||||
],
|
||||
networks: ['my-app-network'],
|
||||
labels: {
|
||||
app: 'api',
|
||||
version: '2.0.0',
|
||||
},
|
||||
resources: {
|
||||
limits: {
|
||||
Memory: 256 * 1024 * 1024, // 256MB
|
||||
CPUs: 0.5,
|
||||
},
|
||||
},
|
||||
secrets: ['api-key', 'db-password'],
|
||||
mounts: [
|
||||
{
|
||||
Target: '/data',
|
||||
Source: 'app-data',
|
||||
Type: 'volume',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log(`Service deployed: ${service.ID}`);
|
||||
```
|
||||
|
||||
#### Manage Services
|
||||
|
||||
```typescript
|
||||
// List all services
|
||||
const services = await docker.getServices();
|
||||
services.forEach((service) => {
|
||||
console.log(`Service: ${service.Spec.Name}`);
|
||||
console.log(` Replicas: ${service.Spec.Mode.Replicated.Replicas}`);
|
||||
console.log(` Image: ${service.Spec.TaskTemplate.ContainerSpec.Image}`);
|
||||
});
|
||||
|
||||
// Get service by name
|
||||
const myService = await DockerService.getServiceByName(docker, 'web-api');
|
||||
|
||||
// Check if service needs update
|
||||
const needsUpdate = await myService.needsUpdate();
|
||||
if (needsUpdate) {
|
||||
console.log('Service configuration has changed, update needed');
|
||||
}
|
||||
|
||||
// Remove service
|
||||
await myService.remove();
|
||||
```
|
||||
|
||||
### 🔐 Secrets Management
|
||||
|
||||
```typescript
|
||||
import { DockerSecret } from '@apiclient.xyz/docker';
|
||||
|
||||
// Create a secret
|
||||
const secret = await DockerSecret.createSecret(docker, {
|
||||
name: 'api-key',
|
||||
data: Buffer.from('super-secret-key-123').toString('base64'),
|
||||
labels: {
|
||||
app: 'my-app',
|
||||
type: 'api-key',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Secret created: ${secret.ID}`);
|
||||
|
||||
// List secrets
|
||||
const secrets = await DockerSecret.getSecrets(docker);
|
||||
secrets.forEach((secret) => {
|
||||
console.log(`Secret: ${secret.Spec.Name}`);
|
||||
});
|
||||
|
||||
// Get secret by name
|
||||
const apiKeySecret = await DockerSecret.getSecretByName(docker, 'api-key');
|
||||
|
||||
// Update secret
|
||||
await apiKeySecret.update({
|
||||
data: Buffer.from('new-secret-key-456').toString('base64'),
|
||||
});
|
||||
|
||||
// Remove secret
|
||||
await apiKeySecret.remove();
|
||||
```
|
||||
|
||||
### 💾 S3 Image Storage
|
||||
|
||||
Store and retrieve Docker images from S3-compatible storage:
|
||||
|
||||
```typescript
|
||||
// Configure S3 storage
|
||||
await docker.addS3Storage({
|
||||
endpoint: 's3.amazonaws.com',
|
||||
accessKeyId: 'your-access-key',
|
||||
secretAccessKey: 'your-secret-key',
|
||||
bucket: 'docker-images',
|
||||
});
|
||||
|
||||
// Store an image to S3
|
||||
const imageStore = docker.imageStore;
|
||||
await imageStore.storeImage('my-app:v1.0.0');
|
||||
|
||||
// Retrieve an image from S3
|
||||
const retrievedImage = await imageStore.getImage('my-app:v1.0.0');
|
||||
```
|
||||
|
||||
### 📊 Event Monitoring
|
||||
|
||||
Monitor Docker events in real-time using RxJS observables:
|
||||
|
||||
```typescript
|
||||
// Subscribe to Docker events
|
||||
const eventStream = docker.getEventObservable();
|
||||
|
||||
const subscription = eventStream.subscribe({
|
||||
next: (event) => {
|
||||
console.log(`Event: ${event.Type} - ${event.Action}`);
|
||||
console.log(`Actor: ${event.Actor.ID}`);
|
||||
console.log(`Time: ${new Date(event.time * 1000).toISOString()}`);
|
||||
},
|
||||
error: (err) => console.error('Event stream error:', err),
|
||||
complete: () => console.log('Event stream completed'),
|
||||
});
|
||||
|
||||
// Unsubscribe when done
|
||||
subscription.unsubscribe();
|
||||
```
|
||||
|
||||
### 🔧 Registry Authentication
|
||||
|
||||
Authenticate with Docker registries for private images:
|
||||
|
||||
```typescript
|
||||
// Authenticate with Docker Hub
|
||||
await docker.auth({
|
||||
username: 'your-username',
|
||||
password: 'your-password',
|
||||
serveraddress: 'https://index.docker.io/v1/',
|
||||
});
|
||||
|
||||
// Or use existing Docker config
|
||||
const authToken = await docker.getAuthTokenFromDockerConfig('myregistry.com');
|
||||
|
||||
// Use auth token when pulling images
|
||||
const privateImage = await DockerImage.createFromRegistry(docker, {
|
||||
imageName: 'myregistry.com/private/image',
|
||||
imageTag: 'latest',
|
||||
authToken,
|
||||
});
|
||||
```
|
||||
|
||||
### 🔄 Swarm Mode
|
||||
|
||||
Initialize and manage Docker Swarm:
|
||||
|
||||
```typescript
|
||||
// Initialize swarm mode
|
||||
await docker.activateSwarm({
|
||||
ListenAddr: '0.0.0.0:2377',
|
||||
AdvertiseAddr: '192.168.1.100:2377',
|
||||
ForceNewCluster: false,
|
||||
});
|
||||
|
||||
// Now you can create services, secrets, and use swarm features
|
||||
const service = await DockerService.createService(docker, {
|
||||
name: 'my-swarm-service',
|
||||
image: 'nginx:latest',
|
||||
replicas: 5,
|
||||
// ... more service config
|
||||
});
|
||||
```
|
||||
|
||||
## 🏗️ Advanced Examples
|
||||
|
||||
### Complete Application Stack
|
||||
|
||||
```typescript
|
||||
async function deployStack() {
|
||||
const docker = new DockerHost();
|
||||
|
||||
// Create network
|
||||
const network = await DockerNetwork.createNetwork(docker, {
|
||||
Name: 'app-network',
|
||||
Driver: 'overlay', // for swarm mode
|
||||
});
|
||||
console.log(`Network Created: ${network.Id}`);
|
||||
}
|
||||
|
||||
listAndCreateNetwork();
|
||||
// Create secrets
|
||||
const dbPassword = await DockerSecret.createSecret(docker, {
|
||||
name: 'db-password',
|
||||
data: Buffer.from('strong-password').toString('base64'),
|
||||
});
|
||||
|
||||
// Deploy database service
|
||||
const dbService = await DockerService.createService(docker, {
|
||||
name: 'postgres',
|
||||
image: 'postgres:14',
|
||||
networks: ['app-network'],
|
||||
secrets: ['db-password'],
|
||||
env: ['POSTGRES_PASSWORD_FILE=/run/secrets/db-password'],
|
||||
});
|
||||
|
||||
// Deploy application service
|
||||
const appService = await DockerService.createService(docker, {
|
||||
name: 'web-app',
|
||||
image: 'my-app:latest',
|
||||
replicas: 3,
|
||||
networks: ['app-network'],
|
||||
ports: [{ Protocol: 'tcp', PublishedPort: 80, TargetPort: 3000 }],
|
||||
});
|
||||
|
||||
console.log('Stack deployed successfully!');
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
## 🔍 TypeScript Support
|
||||
|
||||
You can leverage the full potential of the Docker Remote API with `@apiclient.xyz/docker`. This includes managing images, volumes, swarms, and more. The package's design is consistent and intuitive, making it easy to extend your usage as needed.
|
||||
This package provides comprehensive TypeScript definitions for all Docker API entities:
|
||||
|
||||
Remember, the Docker Remote API offers extensive capabilities. Always refer to the [Docker API documentation](https://docs.docker.com/engine/api/latest/) for a comprehensive list of endpoints and actions you can perform.
|
||||
```typescript
|
||||
import type {
|
||||
IContainerCreationDescriptor,
|
||||
IServiceCreationDescriptor,
|
||||
INetworkCreationDescriptor,
|
||||
IImageCreationDescriptor,
|
||||
ISecretCreationDescriptor,
|
||||
} from '@apiclient.xyz/docker';
|
||||
|
||||
### Conclusion
|
||||
// Full IntelliSense support for all configuration options
|
||||
const containerConfig: IContainerCreationDescriptor = {
|
||||
Image: 'node:18',
|
||||
// Your IDE will provide full autocomplete here
|
||||
};
|
||||
```
|
||||
|
||||
`@apiclient.xyz/docker` simplifies interaction with Docker's Remote API in TypeScript projects, providing strong typing and asynchronous operations. Whether you're managing containers, images, services or networks, it offers a comprehensive toolset to perform these tasks seamlessly.
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please feel free to submit issues and pull requests.
|
||||
|
||||
## 📖 API Documentation
|
||||
|
||||
For complete API documentation, visit [https://apiclient.xyz/docker](https://apiclient.xyz/docker)
|
||||
|
||||
For Docker Remote API reference, see [Docker Engine API Documentation](https://docs.docker.com/engine/api/latest/)
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
|
40
test-stream.js
Normal file
40
test-stream.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { SmartRequest } = require('@push.rocks/smartrequest');
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
const response = await SmartRequest.create()
|
||||
.url('http://unix:/run/user/1000/docker.sock:/images/hello-world:latest/get')
|
||||
.header('Host', 'docker.sock')
|
||||
.get();
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response type:', typeof response);
|
||||
|
||||
const stream = response.streamNode();
|
||||
console.log('Stream type:', typeof stream);
|
||||
console.log('Has on method:', typeof stream.on);
|
||||
|
||||
if (stream) {
|
||||
let chunks = 0;
|
||||
stream.on('data', (chunk) => {
|
||||
chunks++;
|
||||
if (chunks <= 3) console.log('Got chunk', chunks, chunk.length);
|
||||
});
|
||||
stream.on('end', () => {
|
||||
console.log('Stream ended, total chunks:', chunks);
|
||||
process.exit(0);
|
||||
});
|
||||
stream.on('error', (err) => {
|
||||
console.error('Stream error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
console.log('No stream available');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
test();
|
46
test-stream.mjs
Normal file
46
test-stream.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
const response = await SmartRequest.create()
|
||||
.url('http://unix:/run/user/1000/docker.sock:/images/hello-world:latest/get')
|
||||
.header('Host', 'docker.sock')
|
||||
.get();
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response type:', typeof response);
|
||||
|
||||
const stream = response.streamNode();
|
||||
console.log('Stream type:', typeof stream);
|
||||
console.log('Has on method:', typeof stream.on);
|
||||
|
||||
if (stream) {
|
||||
let chunks = 0;
|
||||
stream.on('data', (chunk) => {
|
||||
chunks++;
|
||||
if (chunks <= 3) console.log('Got chunk', chunks, chunk.length);
|
||||
});
|
||||
stream.on('end', () => {
|
||||
console.log('Stream ended, total chunks:', chunks);
|
||||
process.exit(0);
|
||||
});
|
||||
stream.on('error', (err) => {
|
||||
console.error('Stream error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Set a timeout in case stream doesn't end
|
||||
setTimeout(() => {
|
||||
console.log('Timeout after 5 seconds');
|
||||
process.exit(1);
|
||||
}, 5000);
|
||||
} else {
|
||||
console.log('No stream available');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
test();
|
@@ -1,4 +1,7 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { Qenv } from '@push.rocks/qenv';
|
||||
|
||||
const testQenv = new Qenv('./', './.nogit/');
|
||||
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
import * as paths from '../ts/paths.js';
|
||||
@@ -9,6 +12,7 @@ let testDockerHost: docker.DockerHost;
|
||||
|
||||
tap.test('should create a new Dockersock instance', async () => {
|
||||
testDockerHost = new docker.DockerHost({});
|
||||
await testDockerHost.start();
|
||||
return expect(testDockerHost).toBeInstanceOf(docker.DockerHost);
|
||||
});
|
||||
|
||||
@@ -37,7 +41,10 @@ tap.test('should create a network', async () => {
|
||||
});
|
||||
|
||||
tap.test('should remove a network', async () => {
|
||||
const webgateway = await docker.DockerNetwork.getNetworkByName(testDockerHost, 'webgateway');
|
||||
const webgateway = await docker.DockerNetwork.getNetworkByName(
|
||||
testDockerHost,
|
||||
'webgateway',
|
||||
);
|
||||
await webgateway.remove();
|
||||
});
|
||||
|
||||
@@ -74,7 +81,10 @@ tap.test('should create a secret', async () => {
|
||||
});
|
||||
|
||||
tap.test('should remove a secret by name', async () => {
|
||||
const mySecret = await docker.DockerSecret.getSecretByName(testDockerHost, 'testSecret');
|
||||
const mySecret = await docker.DockerSecret.getSecretByName(
|
||||
testDockerHost,
|
||||
'testSecret',
|
||||
);
|
||||
await mySecret.remove();
|
||||
});
|
||||
|
||||
@@ -98,11 +108,14 @@ tap.test('should create a service', async () => {
|
||||
labels: {},
|
||||
contentArg: '{"hi": "wow"}',
|
||||
});
|
||||
const testImage = await docker.DockerImage.createFromRegistry(testDockerHost, {
|
||||
creationObject: {
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
}
|
||||
});
|
||||
const testImage = await docker.DockerImage.createFromRegistry(
|
||||
testDockerHost,
|
||||
{
|
||||
creationObject: {
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
},
|
||||
},
|
||||
);
|
||||
const testService = await docker.DockerService.createService(testDockerHost, {
|
||||
image: testImage,
|
||||
labels: {},
|
||||
@@ -120,13 +133,16 @@ tap.test('should create a service', async () => {
|
||||
|
||||
tap.test('should export images', async (toolsArg) => {
|
||||
const done = toolsArg.defer();
|
||||
const testImage = await docker.DockerImage.createFromRegistry(testDockerHost, {
|
||||
creationObject: {
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
}
|
||||
});
|
||||
const testImage = await docker.DockerImage.createFromRegistry(
|
||||
testDockerHost,
|
||||
{
|
||||
creationObject: {
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
},
|
||||
},
|
||||
);
|
||||
const fsWriteStream = plugins.smartfile.fsStream.createWriteStream(
|
||||
plugins.path.join(paths.nogitDir, 'testimage.tar')
|
||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||
);
|
||||
const exportStream = await testImage.exportToTarStream();
|
||||
exportStream.pipe(fsWriteStream).on('finish', () => {
|
||||
@@ -135,17 +151,43 @@ tap.test('should export images', async (toolsArg) => {
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('should import images', async (toolsArg) => {
|
||||
const done = toolsArg.defer();
|
||||
tap.test('should import images', async () => {
|
||||
const fsReadStream = plugins.smartfile.fsStream.createReadStream(
|
||||
plugins.path.join(paths.nogitDir, 'testimage.tar')
|
||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||
);
|
||||
await docker.DockerImage.createFromTarStream(testDockerHost, {
|
||||
tarStream: fsReadStream,
|
||||
creationObject: {
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
}
|
||||
})
|
||||
})
|
||||
const importedImage = await docker.DockerImage.createFromTarStream(
|
||||
testDockerHost,
|
||||
{
|
||||
tarStream: fsReadStream,
|
||||
creationObject: {
|
||||
imageUrl: 'code.foss.global/host.today/ht-docker-node:latest',
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(importedImage).toBeInstanceOf(docker.DockerImage);
|
||||
});
|
||||
|
||||
tap.test('should expose a working DockerImageStore', async () => {
|
||||
// lets first add am s3 target
|
||||
const s3Descriptor = {
|
||||
endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'),
|
||||
accessKey: await testQenv.getEnvVarOnDemand('S3_ACCESSKEY'),
|
||||
accessSecret: await testQenv.getEnvVarOnDemand('S3_ACCESSSECRET'),
|
||||
bucketName: await testQenv.getEnvVarOnDemand('S3_BUCKET'),
|
||||
};
|
||||
await testDockerHost.addS3Storage(s3Descriptor);
|
||||
|
||||
//
|
||||
await testDockerHost.imageStore.storeImage(
|
||||
'hello2',
|
||||
plugins.smartfile.fsStream.createReadStream(
|
||||
plugins.path.join(paths.nogitDir, 'testimage.tar'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
tap.test('cleanup', async () => {
|
||||
await testDockerHost.stop();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@apiclient.xyz/docker',
|
||||
version: '1.1.4',
|
||||
version: '1.3.5',
|
||||
description: 'Provides easy communication with Docker remote API from Node.js, with TypeScript support.'
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import * as plugins from './plugins.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import { DockerHost } from './classes.host.js';
|
||||
import { logger } from './logging.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
export class DockerContainer {
|
||||
// STATIC
|
||||
@@ -10,7 +10,9 @@ export class DockerContainer {
|
||||
/**
|
||||
* get all containers
|
||||
*/
|
||||
public static async getContainers(dockerHostArg: DockerHost): Promise<DockerContainer[]> {
|
||||
public static async getContainers(
|
||||
dockerHostArg: DockerHost,
|
||||
): Promise<DockerContainer[]> {
|
||||
const result: DockerContainer[] = [];
|
||||
const response = await dockerHostArg.request('GET', '/containers/json');
|
||||
|
||||
@@ -34,7 +36,7 @@ export class DockerContainer {
|
||||
*/
|
||||
public static async create(
|
||||
dockerHost: DockerHost,
|
||||
containerCreationDescriptor: interfaces.IContainerCreationDescriptor
|
||||
containerCreationDescriptor: interfaces.IContainerCreationDescriptor,
|
||||
) {
|
||||
// check for unique hostname
|
||||
const existingContainers = await DockerContainer.getContainers(dockerHost);
|
||||
@@ -50,7 +52,10 @@ export class DockerContainer {
|
||||
if (response.statusCode < 300) {
|
||||
logger.log('info', 'Container created successfully');
|
||||
} else {
|
||||
logger.log('error', 'There has been a problem when creating the container');
|
||||
logger.log(
|
||||
'error',
|
||||
'There has been a problem when creating the container',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import { DockerContainer } from './classes.container.js';
|
||||
import { DockerNetwork } from './classes.network.js';
|
||||
import { DockerService } from './classes.service.js';
|
||||
import { logger } from './logging.js';
|
||||
import { logger } from './logger.js';
|
||||
import path from 'path';
|
||||
import type { DockerImageStore } from './classes.imagestore.js';
|
||||
import { DockerImageStore } from './classes.imagestore.js';
|
||||
import { DockerImage } from './classes.image.js';
|
||||
|
||||
export interface IAuthData {
|
||||
serveraddress: string;
|
||||
@@ -18,18 +20,30 @@ export interface IDockerHostConstructorOptions {
|
||||
}
|
||||
|
||||
export class DockerHost {
|
||||
public options: IDockerHostConstructorOptions;
|
||||
|
||||
/**
|
||||
* the path where the docker sock can be found
|
||||
*/
|
||||
public socketPath: string;
|
||||
private registryToken: string = '';
|
||||
public imageStore: DockerImageStore;
|
||||
public smartBucket: plugins.smartbucket.SmartBucket;
|
||||
|
||||
/**
|
||||
* the constructor to instantiate a new docker sock instance
|
||||
* @param pathArg
|
||||
*/
|
||||
constructor(optionsArg: IDockerHostConstructorOptions) {
|
||||
this.options = {
|
||||
...{
|
||||
imageStoreDir: plugins.path.join(
|
||||
paths.nogitDir,
|
||||
'temp-docker-image-store',
|
||||
),
|
||||
},
|
||||
...optionsArg,
|
||||
};
|
||||
let pathToUse: string;
|
||||
if (optionsArg.dockerSockPath) {
|
||||
pathToUse = optionsArg.dockerSockPath;
|
||||
@@ -48,6 +62,17 @@ export class DockerHost {
|
||||
}
|
||||
console.log(`using docker sock at ${pathToUse}`);
|
||||
this.socketPath = pathToUse;
|
||||
this.imageStore = new DockerImageStore({
|
||||
bucketDir: null,
|
||||
localDirPath: this.options.imageStoreDir,
|
||||
});
|
||||
}
|
||||
|
||||
public async start() {
|
||||
await this.imageStore.start();
|
||||
}
|
||||
public async stop() {
|
||||
await this.imageStore.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,17 +87,22 @@ export class DockerHost {
|
||||
throw new Error(response.body.Status);
|
||||
}
|
||||
console.log(response.body.Status);
|
||||
this.registryToken = plugins.smartstring.base64.encode(plugins.smartjson.stringify(authData));
|
||||
this.registryToken = plugins.smartstring.base64.encode(
|
||||
plugins.smartjson.stringify(authData),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the token from the .docker/config.json file for GitLab registry
|
||||
*/
|
||||
public async getAuthTokenFromDockerConfig(registryUrlArg: string) {
|
||||
const dockerConfigPath = plugins.smartpath.get.home('~/.docker/config.json');
|
||||
const dockerConfigPath = plugins.smartpath.get.home(
|
||||
'~/.docker/config.json',
|
||||
);
|
||||
const configObject = plugins.smartfile.fs.toObjectSync(dockerConfigPath);
|
||||
const gitlabAuthBase64 = configObject.auths[registryUrlArg].auth;
|
||||
const gitlabAuth: string = plugins.smartstring.base64.decode(gitlabAuthBase64);
|
||||
const gitlabAuth: string =
|
||||
plugins.smartstring.base64.decode(gitlabAuthBase64);
|
||||
const gitlabAuthArray = gitlabAuth.split(':');
|
||||
await this.auth({
|
||||
username: gitlabAuthArray[0],
|
||||
@@ -81,6 +111,9 @@ export class DockerHost {
|
||||
});
|
||||
}
|
||||
|
||||
// ==============
|
||||
// NETWORKS
|
||||
// ==============
|
||||
/**
|
||||
* gets all networks
|
||||
*/
|
||||
@@ -89,9 +122,24 @@ export class DockerHost {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* create a network
|
||||
*/
|
||||
public async createNetwork(
|
||||
optionsArg: Parameters<typeof DockerNetwork.createNetwork>[1],
|
||||
) {
|
||||
return await DockerNetwork.createNetwork(this, optionsArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* get a network by name
|
||||
*/
|
||||
public async getNetworkByName(networkNameArg: string) {
|
||||
return await DockerNetwork.getNetworkByName(this, networkNameArg);
|
||||
}
|
||||
|
||||
// ==============
|
||||
// CONTAINERS
|
||||
// ==============
|
||||
/**
|
||||
* gets all containers
|
||||
*/
|
||||
@@ -100,6 +148,10 @@ export class DockerHost {
|
||||
return containerArray;
|
||||
}
|
||||
|
||||
// ==============
|
||||
// SERVICES
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* gets all services
|
||||
*/
|
||||
@@ -108,6 +160,24 @@ export class DockerHost {
|
||||
return serviceArray;
|
||||
}
|
||||
|
||||
// ==============
|
||||
// IMAGES
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* get all images
|
||||
*/
|
||||
public async getImages() {
|
||||
return await DockerImage.getImages(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* get an image by name
|
||||
*/
|
||||
public async getImageByName(imageNameArg: string) {
|
||||
return await DockerImage.getImageByName(this, imageNameArg);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -165,53 +235,180 @@ export class DockerHost {
|
||||
*/
|
||||
public async request(methodArg: string, routeArg: string, dataArg = {}) {
|
||||
const requestUrl = `${this.socketPath}${routeArg}`;
|
||||
const response = await plugins.smartrequest.request(requestUrl, {
|
||||
method: methodArg,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Registry-Auth': this.registryToken,
|
||||
Host: 'docker.sock',
|
||||
},
|
||||
requestBody: dataArg,
|
||||
keepAlive: false,
|
||||
});
|
||||
if (response.statusCode !== 200) {
|
||||
console.log(response.body);
|
||||
|
||||
// Build the request using the fluent API
|
||||
const smartRequest = plugins.smartrequest.SmartRequest.create()
|
||||
.url(requestUrl)
|
||||
.header('Content-Type', 'application/json')
|
||||
.header('X-Registry-Auth', this.registryToken)
|
||||
.header('Host', 'docker.sock')
|
||||
.options({ keepAlive: false });
|
||||
|
||||
// Add body for methods that support it
|
||||
if (dataArg && Object.keys(dataArg).length > 0) {
|
||||
smartRequest.json(dataArg);
|
||||
}
|
||||
return response;
|
||||
|
||||
// Execute the request based on method
|
||||
let response;
|
||||
switch (methodArg.toUpperCase()) {
|
||||
case 'GET':
|
||||
response = await smartRequest.get();
|
||||
break;
|
||||
case 'POST':
|
||||
response = await smartRequest.post();
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await smartRequest.put();
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await smartRequest.delete();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported HTTP method: ${methodArg}`);
|
||||
}
|
||||
|
||||
// Parse the response body based on content type
|
||||
let body;
|
||||
const contentType = response.headers['content-type'] || '';
|
||||
|
||||
// Docker's streaming endpoints (like /images/create) return newline-delimited JSON
|
||||
// which can't be parsed as a single JSON object
|
||||
const isStreamingEndpoint =
|
||||
routeArg.includes('/images/create') ||
|
||||
routeArg.includes('/images/load') ||
|
||||
routeArg.includes('/build');
|
||||
|
||||
if (contentType.includes('application/json') && !isStreamingEndpoint) {
|
||||
body = await response.json();
|
||||
} else {
|
||||
body = await response.text();
|
||||
// Try to parse as JSON if it looks like JSON and is not a streaming response
|
||||
if (
|
||||
!isStreamingEndpoint &&
|
||||
body &&
|
||||
(body.startsWith('{') || body.startsWith('['))
|
||||
) {
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
} catch {
|
||||
// Keep as text if parsing fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a response object compatible with existing code
|
||||
const legacyResponse = {
|
||||
statusCode: response.status,
|
||||
body: body,
|
||||
headers: response.headers,
|
||||
};
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log(body);
|
||||
}
|
||||
|
||||
return legacyResponse;
|
||||
}
|
||||
|
||||
public async requestStreaming(methodArg: string, routeArg: string, readStream?: plugins.smartstream.stream.Readable) {
|
||||
public async requestStreaming(
|
||||
methodArg: string,
|
||||
routeArg: string,
|
||||
readStream?: plugins.smartstream.stream.Readable,
|
||||
) {
|
||||
const requestUrl = `${this.socketPath}${routeArg}`;
|
||||
const response = await plugins.smartrequest.request(
|
||||
requestUrl,
|
||||
{
|
||||
method: methodArg,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Registry-Auth': this.registryToken,
|
||||
Host: 'docker.sock',
|
||||
},
|
||||
requestBody: null,
|
||||
keepAlive: false,
|
||||
},
|
||||
true,
|
||||
(readStream ? reqArg => {
|
||||
let counter = 0;
|
||||
const smartduplex = new plugins.smartstream.SmartDuplex({
|
||||
writeFunction: async (chunkArg) => {
|
||||
if (counter % 1000 === 0) {
|
||||
console.log(`posting chunk ${counter}`);
|
||||
}
|
||||
counter++;
|
||||
return chunkArg;
|
||||
|
||||
// Build the request using the fluent API
|
||||
const smartRequest = plugins.smartrequest.SmartRequest.create()
|
||||
.url(requestUrl)
|
||||
.header('Content-Type', 'application/json')
|
||||
.header('X-Registry-Auth', this.registryToken)
|
||||
.header('Host', 'docker.sock')
|
||||
.timeout(30000)
|
||||
.options({ keepAlive: false, autoDrain: true }); // Disable auto-drain for streaming
|
||||
|
||||
// If we have a readStream, use the new stream method with logging
|
||||
if (readStream) {
|
||||
let counter = 0;
|
||||
const smartduplex = new plugins.smartstream.SmartDuplex({
|
||||
writeFunction: async (chunkArg) => {
|
||||
if (counter % 1000 === 0) {
|
||||
console.log(`posting chunk ${counter}`);
|
||||
}
|
||||
});
|
||||
readStream.pipe(smartduplex).pipe(reqArg);
|
||||
} : null),
|
||||
counter++;
|
||||
return chunkArg;
|
||||
},
|
||||
});
|
||||
|
||||
// Pipe through the logging duplex stream
|
||||
const loggedStream = readStream.pipe(smartduplex);
|
||||
|
||||
// Use the new stream method to stream the data
|
||||
smartRequest.stream(loggedStream, 'application/octet-stream');
|
||||
}
|
||||
|
||||
// Execute the request based on method
|
||||
let response;
|
||||
switch (methodArg.toUpperCase()) {
|
||||
case 'GET':
|
||||
response = await smartRequest.get();
|
||||
break;
|
||||
case 'POST':
|
||||
response = await smartRequest.post();
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await smartRequest.put();
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await smartRequest.delete();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported HTTP method: ${methodArg}`);
|
||||
}
|
||||
|
||||
console.log(response.status);
|
||||
|
||||
// For streaming responses, get the Node.js stream
|
||||
const nodeStream = response.streamNode();
|
||||
|
||||
if (!nodeStream) {
|
||||
// If no stream is available, consume the body as text
|
||||
const body = await response.text();
|
||||
console.log(body);
|
||||
|
||||
// Return a compatible response object
|
||||
return {
|
||||
statusCode: response.status,
|
||||
body: body,
|
||||
headers: response.headers,
|
||||
};
|
||||
}
|
||||
|
||||
// For streaming responses, return the stream with added properties
|
||||
(nodeStream as any).statusCode = response.status;
|
||||
(nodeStream as any).body = ''; // For compatibility
|
||||
|
||||
return nodeStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* add s3 storage
|
||||
* @param optionsArg
|
||||
*/
|
||||
public async addS3Storage(optionsArg: plugins.tsclass.storage.IS3Descriptor) {
|
||||
this.smartBucket = new plugins.smartbucket.SmartBucket(optionsArg);
|
||||
if (!optionsArg.bucketName) {
|
||||
throw new Error('bucketName is required');
|
||||
}
|
||||
const bucket = await this.smartBucket.getBucketByName(
|
||||
optionsArg.bucketName,
|
||||
);
|
||||
console.log(response.statusCode);
|
||||
console.log(response.body);
|
||||
return response;
|
||||
let wantedDirectory = await bucket.getBaseDirectory();
|
||||
if (optionsArg.directoryPath) {
|
||||
wantedDirectory = await wantedDirectory.getSubDirectoryByName(
|
||||
optionsArg.directoryPath,
|
||||
);
|
||||
}
|
||||
this.imageStore.options.bucketDir = wantedDirectory;
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,11 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import { DockerHost } from './classes.host.js';
|
||||
import { logger } from './logging.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* represents a docker image on the remote docker host
|
||||
*/
|
||||
export class DockerImage {
|
||||
// STATIC
|
||||
public static async getImages(dockerHost: DockerHost) {
|
||||
@@ -14,7 +17,10 @@ export class DockerImage {
|
||||
return images;
|
||||
}
|
||||
|
||||
public static async findImageByName(dockerHost: DockerHost, imageNameArg: string) {
|
||||
public static async getImageByName(
|
||||
dockerHost: DockerHost,
|
||||
imageNameArg: string,
|
||||
) {
|
||||
const images = await this.getImages(dockerHost);
|
||||
const result = images.find((image) => {
|
||||
if (image.RepoTags) {
|
||||
@@ -29,8 +35,8 @@ export class DockerImage {
|
||||
public static async createFromRegistry(
|
||||
dockerHostArg: DockerHost,
|
||||
optionsArg: {
|
||||
creationObject: interfaces.IImageCreationDescriptor
|
||||
}
|
||||
creationObject: interfaces.IImageCreationDescriptor;
|
||||
},
|
||||
): Promise<DockerImage> {
|
||||
// lets create a sanatized imageUrlObject
|
||||
const imageUrlObject: {
|
||||
@@ -47,7 +53,7 @@ export class DockerImage {
|
||||
const imageTag = imageUrlObject.imageUrl.split(':')[1];
|
||||
if (imageUrlObject.imageTag) {
|
||||
throw new Error(
|
||||
`imageUrl ${imageUrlObject.imageUrl} can't be tagged with ${imageUrlObject.imageTag} because it is already tagged with ${imageTag}`
|
||||
`imageUrl ${imageUrlObject.imageUrl} can't be tagged with ${imageUrlObject.imageTag} because it is already tagged with ${imageTag}`,
|
||||
);
|
||||
} else {
|
||||
imageUrlObject.imageUrl = imageUrl;
|
||||
@@ -62,12 +68,18 @@ export class DockerImage {
|
||||
const response = await dockerHostArg.request(
|
||||
'POST',
|
||||
`/images/create?fromImage=${encodeURIComponent(
|
||||
imageUrlObject.imageUrl
|
||||
)}&tag=${encodeURIComponent(imageUrlObject.imageTag)}`
|
||||
imageUrlObject.imageUrl,
|
||||
)}&tag=${encodeURIComponent(imageUrlObject.imageTag)}`,
|
||||
);
|
||||
if (response.statusCode < 300) {
|
||||
logger.log('info', `Successfully pulled image ${imageUrlObject.imageUrl} from the registry`);
|
||||
const image = await DockerImage.findImageByName(dockerHostArg, imageUrlObject.imageOriginTag);
|
||||
logger.log(
|
||||
'info',
|
||||
`Successfully pulled image ${imageUrlObject.imageUrl} from the registry`,
|
||||
);
|
||||
const image = await DockerImage.getImageByName(
|
||||
dockerHostArg,
|
||||
imageUrlObject.imageOriginTag,
|
||||
);
|
||||
return image;
|
||||
} else {
|
||||
logger.log('error', `Failed at the attempt of creating a new image`);
|
||||
@@ -79,22 +91,101 @@ export class DockerImage {
|
||||
* @param dockerHostArg
|
||||
* @param tarStreamArg
|
||||
*/
|
||||
public static async createFromTarStream(dockerHostArg: DockerHost, optionsArg: {
|
||||
creationObject: interfaces.IImageCreationDescriptor,
|
||||
tarStream: plugins.smartstream.stream.Readable,
|
||||
}) {
|
||||
const response = await dockerHostArg.requestStreaming('POST', '/images/load', optionsArg.tarStream);
|
||||
return response;
|
||||
public static async createFromTarStream(
|
||||
dockerHostArg: DockerHost,
|
||||
optionsArg: {
|
||||
creationObject: interfaces.IImageCreationDescriptor;
|
||||
tarStream: plugins.smartstream.stream.Readable;
|
||||
},
|
||||
): Promise<DockerImage> {
|
||||
// Start the request for importing an image
|
||||
const response = await dockerHostArg.requestStreaming(
|
||||
'POST',
|
||||
'/images/load',
|
||||
optionsArg.tarStream,
|
||||
);
|
||||
|
||||
/**
|
||||
* Docker typically returns lines like:
|
||||
* {"stream":"Loaded image: myrepo/myimage:latest"}
|
||||
*
|
||||
* So we will collect those lines and parse out the final image name.
|
||||
*/
|
||||
let rawOutput = '';
|
||||
response.on('data', (chunk) => {
|
||||
rawOutput += chunk.toString();
|
||||
});
|
||||
|
||||
// Wrap the end event in a Promise for easier async/await usage
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
response.on('end', () => {
|
||||
resolve();
|
||||
});
|
||||
response.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
// Attempt to parse each line to find something like "Loaded image: ..."
|
||||
let loadedImageTag: string | undefined;
|
||||
const lines = rawOutput.trim().split('\n').filter(Boolean);
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const jsonLine = JSON.parse(line);
|
||||
if (
|
||||
jsonLine.stream &&
|
||||
(jsonLine.stream.startsWith('Loaded image:') ||
|
||||
jsonLine.stream.startsWith('Loaded image ID:'))
|
||||
) {
|
||||
// Examples:
|
||||
// "Loaded image: your-image:latest"
|
||||
// "Loaded image ID: sha256:...."
|
||||
loadedImageTag = jsonLine.stream
|
||||
.replace('Loaded image: ', '')
|
||||
.replace('Loaded image ID: ', '')
|
||||
.trim();
|
||||
}
|
||||
} catch {
|
||||
// not valid JSON, ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (!loadedImageTag) {
|
||||
throw new Error(
|
||||
`Could not parse the loaded image info from Docker response.\nResponse was:\n${rawOutput}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Now try to look up that image by the "loadedImageTag".
|
||||
// Depending on Docker’s response, it might be something like:
|
||||
// "myrepo/myimage:latest" OR "sha256:someHash..."
|
||||
// If Docker gave you an ID (e.g. "sha256:..."), you may need a separate
|
||||
// DockerImage.getImageById method; or if you prefer, you can treat it as a name.
|
||||
const newlyImportedImage = await DockerImage.getImageByName(
|
||||
dockerHostArg,
|
||||
loadedImageTag,
|
||||
);
|
||||
|
||||
if (!newlyImportedImage) {
|
||||
throw new Error(
|
||||
`Image load succeeded, but no local reference found for "${loadedImageTag}".`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.log('info', `Successfully imported image "${loadedImageTag}".`);
|
||||
|
||||
return newlyImportedImage;
|
||||
}
|
||||
|
||||
public static async tagImageByIdOrName(
|
||||
dockerHost: DockerHost,
|
||||
idOrNameArg: string,
|
||||
newTagArg: string
|
||||
newTagArg: string,
|
||||
) {
|
||||
const response = await dockerHost.request(
|
||||
'POST',
|
||||
`/images/${encodeURIComponent(idOrNameArg)}/${encodeURIComponent(newTagArg)}`
|
||||
`/images/${encodeURIComponent(idOrNameArg)}/${encodeURIComponent(newTagArg)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,28 +254,44 @@ export class DockerImage {
|
||||
* exports an image to a tar ball
|
||||
*/
|
||||
public async exportToTarStream(): Promise<plugins.smartstream.stream.Readable> {
|
||||
console.log(`Exporting image ${this.RepoTags[0]} to tar stream.`);
|
||||
const response = await this.dockerHost.requestStreaming('GET', `/images/${encodeURIComponent(this.RepoTags[0])}/get`);
|
||||
logger.log('info', `Exporting image ${this.RepoTags[0]} to tar stream.`);
|
||||
const response = await this.dockerHost.requestStreaming(
|
||||
'GET',
|
||||
`/images/${encodeURIComponent(this.RepoTags[0])}/get`,
|
||||
);
|
||||
|
||||
// Check if response is a Node.js stream
|
||||
if (!response || typeof response.on !== 'function') {
|
||||
throw new Error('Failed to get streaming response for image export');
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
const webduplexStream = new plugins.smartstream.SmartDuplex({
|
||||
writeFunction: async (chunk, tools) => {
|
||||
if (counter % 1000 === 0)
|
||||
console.log(`Got chunk: ${counter}`);
|
||||
if (counter % 1000 === 0) console.log(`Got chunk: ${counter}`);
|
||||
counter++;
|
||||
return chunk;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
if (!webduplexStream.write(chunk)) {
|
||||
response.pause();
|
||||
webduplexStream.once('drain', () => {
|
||||
response.resume();
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
webduplexStream.end();
|
||||
})
|
||||
});
|
||||
|
||||
response.on('error', (error) => {
|
||||
logger.log('error', `Error during image export: ${error.message}`);
|
||||
webduplexStream.destroy(error);
|
||||
});
|
||||
|
||||
return webduplexStream;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import { logger } from './logger.js';
|
||||
import type { DockerHost } from './classes.host.js';
|
||||
|
||||
export interface IDockerImageStoreConstructorOptions {
|
||||
@@ -16,28 +17,142 @@ export interface IDockerImageStoreConstructorOptions {
|
||||
export class DockerImageStore {
|
||||
public options: IDockerImageStoreConstructorOptions;
|
||||
|
||||
constructor(dockerHost: DockerHost, optionsArg: IDockerImageStoreConstructorOptions) {
|
||||
constructor(optionsArg: IDockerImageStoreConstructorOptions) {
|
||||
this.options = optionsArg;
|
||||
}
|
||||
|
||||
// Method to store tar stream
|
||||
public async storeImage(imageName: string, tarStream: plugins.smartstream.stream.Readable): Promise<void> {
|
||||
const imagePath = plugins.path.join(this.options.localDirPath, `${imageName}.tar`);
|
||||
public async storeImage(
|
||||
imageName: string,
|
||||
tarStream: plugins.smartstream.stream.Readable,
|
||||
): Promise<void> {
|
||||
logger.log('info', `Storing image ${imageName}...`);
|
||||
const uniqueProcessingId = plugins.smartunique.shortId();
|
||||
|
||||
const initialTarDownloadPath = plugins.path.join(
|
||||
this.options.localDirPath,
|
||||
`${uniqueProcessingId}.tar`,
|
||||
);
|
||||
const extractionDir = plugins.path.join(
|
||||
this.options.localDirPath,
|
||||
uniqueProcessingId,
|
||||
);
|
||||
// Create a write stream to store the tar file
|
||||
const writeStream = plugins.smartfile.fsStream.createWriteStream(imagePath);
|
||||
const writeStream = plugins.smartfile.fsStream.createWriteStream(
|
||||
initialTarDownloadPath,
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// lets wait for the write stream to finish
|
||||
await new Promise((resolve, reject) => {
|
||||
tarStream.pipe(writeStream);
|
||||
|
||||
writeStream.on('finish', resolve);
|
||||
writeStream.on('error', reject);
|
||||
});
|
||||
logger.log(
|
||||
'info',
|
||||
`Image ${imageName} stored locally for processing. Extracting...`,
|
||||
);
|
||||
|
||||
// lets process the image
|
||||
const tarArchive = await plugins.smartarchive.SmartArchive.fromArchiveFile(
|
||||
initialTarDownloadPath,
|
||||
);
|
||||
await tarArchive.exportToFs(extractionDir);
|
||||
logger.log('info', `Image ${imageName} extracted.`);
|
||||
await plugins.smartfile.fs.remove(initialTarDownloadPath);
|
||||
logger.log('info', `deleted original tar to save space.`);
|
||||
logger.log('info', `now repackaging for s3...`);
|
||||
const smartfileIndexJson = await plugins.smartfile.SmartFile.fromFilePath(
|
||||
plugins.path.join(extractionDir, 'index.json'),
|
||||
);
|
||||
const smartfileManifestJson =
|
||||
await plugins.smartfile.SmartFile.fromFilePath(
|
||||
plugins.path.join(extractionDir, 'manifest.json'),
|
||||
);
|
||||
const smartfileOciLayoutJson =
|
||||
await plugins.smartfile.SmartFile.fromFilePath(
|
||||
plugins.path.join(extractionDir, 'oci-layout'),
|
||||
);
|
||||
const smartfileRepositoriesJson =
|
||||
await plugins.smartfile.SmartFile.fromFilePath(
|
||||
plugins.path.join(extractionDir, 'repositories'),
|
||||
);
|
||||
const indexJson = JSON.parse(smartfileIndexJson.contents.toString());
|
||||
const manifestJson = JSON.parse(smartfileManifestJson.contents.toString());
|
||||
const ociLayoutJson = JSON.parse(
|
||||
smartfileOciLayoutJson.contents.toString(),
|
||||
);
|
||||
const repositoriesJson = JSON.parse(
|
||||
smartfileRepositoriesJson.contents.toString(),
|
||||
);
|
||||
|
||||
indexJson.manifests[0].annotations['io.containerd.image.name'] = imageName;
|
||||
manifestJson[0].RepoTags[0] = imageName;
|
||||
const repoFirstKey = Object.keys(repositoriesJson)[0];
|
||||
const repoFirstValue = repositoriesJson[repoFirstKey];
|
||||
repositoriesJson[imageName] = repoFirstValue;
|
||||
delete repositoriesJson[repoFirstKey];
|
||||
|
||||
smartfileIndexJson.contents = Buffer.from(
|
||||
JSON.stringify(indexJson, null, 2),
|
||||
);
|
||||
smartfileManifestJson.contents = Buffer.from(
|
||||
JSON.stringify(manifestJson, null, 2),
|
||||
);
|
||||
smartfileOciLayoutJson.contents = Buffer.from(
|
||||
JSON.stringify(ociLayoutJson, null, 2),
|
||||
);
|
||||
smartfileRepositoriesJson.contents = Buffer.from(
|
||||
JSON.stringify(repositoriesJson, null, 2),
|
||||
);
|
||||
await Promise.all([
|
||||
smartfileIndexJson.write(),
|
||||
smartfileManifestJson.write(),
|
||||
smartfileOciLayoutJson.write(),
|
||||
smartfileRepositoriesJson.write(),
|
||||
]);
|
||||
|
||||
logger.log('info', 'repackaging archive for s3...');
|
||||
const tartools = new plugins.smartarchive.TarTools();
|
||||
const newTarPack = await tartools.packDirectory(extractionDir);
|
||||
const finalTarName = `${uniqueProcessingId}.processed.tar`;
|
||||
const finalTarPath = plugins.path.join(
|
||||
this.options.localDirPath,
|
||||
finalTarName,
|
||||
);
|
||||
const finalWriteStream =
|
||||
plugins.smartfile.fsStream.createWriteStream(finalTarPath);
|
||||
await new Promise((resolve, reject) => {
|
||||
newTarPack.finalize();
|
||||
newTarPack.pipe(finalWriteStream);
|
||||
finalWriteStream.on('finish', resolve);
|
||||
finalWriteStream.on('error', reject);
|
||||
});
|
||||
logger.log('ok', `Repackaged image ${imageName} for s3.`);
|
||||
await plugins.smartfile.fs.remove(extractionDir);
|
||||
const finalTarReadStream =
|
||||
plugins.smartfile.fsStream.createReadStream(finalTarPath);
|
||||
await this.options.bucketDir.fastPutStream({
|
||||
stream: finalTarReadStream,
|
||||
path: `${imageName}.tar`,
|
||||
});
|
||||
await plugins.smartfile.fs.remove(finalTarPath);
|
||||
}
|
||||
|
||||
public async start() {
|
||||
await plugins.smartfile.fs.ensureEmptyDir(this.options.localDirPath);
|
||||
}
|
||||
|
||||
public async stop() {}
|
||||
|
||||
// Method to retrieve tar stream
|
||||
public async getImage(imageName: string): Promise<plugins.smartstream.stream.Readable> {
|
||||
const imagePath = plugins.path.join(this.options.localDirPath, `${imageName}.tar`);
|
||||
public async getImage(
|
||||
imageName: string,
|
||||
): Promise<plugins.smartstream.stream.Readable> {
|
||||
const imagePath = plugins.path.join(
|
||||
this.options.localDirPath,
|
||||
`${imageName}.tar`,
|
||||
);
|
||||
|
||||
if (!(await plugins.smartfile.fs.fileExists(imagePath))) {
|
||||
throw new Error(`Image ${imageName} does not exist.`);
|
||||
|
@@ -3,10 +3,12 @@ import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import { DockerHost } from './classes.host.js';
|
||||
import { DockerService } from './classes.service.js';
|
||||
import { logger } from './logging.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
export class DockerNetwork {
|
||||
public static async getNetworks(dockerHost: DockerHost): Promise<DockerNetwork[]> {
|
||||
public static async getNetworks(
|
||||
dockerHost: DockerHost,
|
||||
): Promise<DockerNetwork[]> {
|
||||
const dockerNetworks: DockerNetwork[] = [];
|
||||
const response = await dockerHost.request('GET', '/networks');
|
||||
for (const networkObject of response.body) {
|
||||
@@ -17,14 +19,19 @@ export class DockerNetwork {
|
||||
return dockerNetworks;
|
||||
}
|
||||
|
||||
public static async getNetworkByName(dockerHost: DockerHost, dockerNetworkNameArg: string) {
|
||||
public static async getNetworkByName(
|
||||
dockerHost: DockerHost,
|
||||
dockerNetworkNameArg: string,
|
||||
) {
|
||||
const networks = await DockerNetwork.getNetworks(dockerHost);
|
||||
return networks.find((dockerNetwork) => dockerNetwork.Name === dockerNetworkNameArg);
|
||||
return networks.find(
|
||||
(dockerNetwork) => dockerNetwork.Name === dockerNetworkNameArg,
|
||||
);
|
||||
}
|
||||
|
||||
public static async createNetwork(
|
||||
dockerHost: DockerHost,
|
||||
networkCreationDescriptor: interfaces.INetworkCreationDescriptor
|
||||
networkCreationDescriptor: interfaces.INetworkCreationDescriptor,
|
||||
): Promise<DockerNetwork> {
|
||||
const response = await dockerHost.request('POST', '/networks/create', {
|
||||
Name: networkCreationDescriptor.Name,
|
||||
@@ -47,9 +54,15 @@ export class DockerNetwork {
|
||||
});
|
||||
if (response.statusCode < 300) {
|
||||
logger.log('info', 'Created network successfully');
|
||||
return await DockerNetwork.getNetworkByName(dockerHost, networkCreationDescriptor.Name);
|
||||
return await DockerNetwork.getNetworkByName(
|
||||
dockerHost,
|
||||
networkCreationDescriptor.Name,
|
||||
);
|
||||
} else {
|
||||
logger.log('error', 'There has been an error creating the wanted network');
|
||||
logger.log(
|
||||
'error',
|
||||
'There has been an error creating the wanted network',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -75,7 +88,7 @@ export class DockerNetwork {
|
||||
Subnet: string;
|
||||
IPRange: string;
|
||||
Gateway: string;
|
||||
}
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -87,7 +100,10 @@ export class DockerNetwork {
|
||||
* removes the network
|
||||
*/
|
||||
public async remove() {
|
||||
const response = await this.dockerHost.request('DELETE', `/networks/${this.Id}`);
|
||||
const response = await this.dockerHost.request(
|
||||
'DELETE',
|
||||
`/networks/${this.Id}`,
|
||||
);
|
||||
}
|
||||
|
||||
public async getContainersOnNetwork(): Promise<
|
||||
@@ -100,7 +116,10 @@ export class DockerNetwork {
|
||||
}>
|
||||
> {
|
||||
const returnArray = [];
|
||||
const response = await this.dockerHost.request('GET', `/networks/${this.Id}`);
|
||||
const response = await this.dockerHost.request(
|
||||
'GET',
|
||||
`/networks/${this.Id}`,
|
||||
);
|
||||
for (const key of Object.keys(response.body.Containers)) {
|
||||
returnArray.push(response.body.Containers[key]);
|
||||
}
|
||||
|
@@ -22,14 +22,17 @@ export class DockerSecret {
|
||||
return secrets.find((secret) => secret.ID === idArg);
|
||||
}
|
||||
|
||||
public static async getSecretByName(dockerHostArg: DockerHost, nameArg: string) {
|
||||
public static async getSecretByName(
|
||||
dockerHostArg: DockerHost,
|
||||
nameArg: string,
|
||||
) {
|
||||
const secrets = await this.getSecrets(dockerHostArg);
|
||||
return secrets.find((secret) => secret.Spec.Name === nameArg);
|
||||
}
|
||||
|
||||
public static async createSecret(
|
||||
dockerHostArg: DockerHost,
|
||||
secretDescriptor: interfaces.ISecretCreationDescriptor
|
||||
secretDescriptor: interfaces.ISecretCreationDescriptor,
|
||||
) {
|
||||
const labels: interfaces.TLabels = {
|
||||
...secretDescriptor.labels,
|
||||
@@ -45,7 +48,7 @@ export class DockerSecret {
|
||||
Object.assign(newSecretInstance, response.body);
|
||||
Object.assign(
|
||||
newSecretInstance,
|
||||
await DockerSecret.getSecretByID(dockerHostArg, newSecretInstance.ID)
|
||||
await DockerSecret.getSecretByID(dockerHostArg, newSecretInstance.ID),
|
||||
);
|
||||
return newSecretInstance;
|
||||
}
|
||||
@@ -77,7 +80,7 @@ export class DockerSecret {
|
||||
Name: this.Spec.Name,
|
||||
Labels: this.Spec.Labels,
|
||||
Data: plugins.smartstring.base64.encode(contentArg),
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import * as interfaces from './interfaces/index.js';
|
||||
import { DockerHost } from './classes.host.js';
|
||||
import { DockerImage } from './classes.image.js';
|
||||
import { DockerSecret } from './classes.secret.js';
|
||||
import { logger } from './logging.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
export class DockerService {
|
||||
// STATIC
|
||||
@@ -21,7 +21,7 @@ export class DockerService {
|
||||
|
||||
public static async getServiceByName(
|
||||
dockerHost: DockerHost,
|
||||
networkName: string
|
||||
networkName: string,
|
||||
): Promise<DockerService> {
|
||||
const allServices = await DockerService.getServices(dockerHost);
|
||||
const wantedService = allServices.find((service) => {
|
||||
@@ -35,10 +35,13 @@ export class DockerService {
|
||||
*/
|
||||
public static async createService(
|
||||
dockerHost: DockerHost,
|
||||
serviceCreationDescriptor: interfaces.IServiceCreationDescriptor
|
||||
serviceCreationDescriptor: interfaces.IServiceCreationDescriptor,
|
||||
): Promise<DockerService> {
|
||||
// lets get the image
|
||||
logger.log('info', `now creating service ${serviceCreationDescriptor.name}`);
|
||||
logger.log(
|
||||
'info',
|
||||
`now creating service ${serviceCreationDescriptor.name}`,
|
||||
);
|
||||
|
||||
// await serviceCreationDescriptor.image.pullLatestImageFromRegistry();
|
||||
const serviceVersion = await serviceCreationDescriptor.image.getVersion();
|
||||
@@ -71,8 +74,12 @@ export class DockerService {
|
||||
});
|
||||
}
|
||||
|
||||
if (serviceCreationDescriptor.resources && serviceCreationDescriptor.resources.volumeMounts) {
|
||||
for (const volumeMount of serviceCreationDescriptor.resources.volumeMounts) {
|
||||
if (
|
||||
serviceCreationDescriptor.resources &&
|
||||
serviceCreationDescriptor.resources.volumeMounts
|
||||
) {
|
||||
for (const volumeMount of serviceCreationDescriptor.resources
|
||||
.volumeMounts) {
|
||||
mounts.push({
|
||||
Target: volumeMount.containerFsPath,
|
||||
Source: volumeMount.hostFsPath,
|
||||
@@ -89,6 +96,11 @@ export class DockerService {
|
||||
}> = [];
|
||||
|
||||
for (const network of serviceCreationDescriptor.networks) {
|
||||
// Skip null networks (can happen if network creation fails)
|
||||
if (!network) {
|
||||
logger.log('warn', 'Skipping null network in service creation');
|
||||
continue;
|
||||
}
|
||||
networkArray.push({
|
||||
Target: network.Name,
|
||||
Aliases: [serviceCreationDescriptor.networkAlias],
|
||||
@@ -125,7 +137,8 @@ export class DockerService {
|
||||
// lets configure limits
|
||||
|
||||
const memoryLimitMB =
|
||||
serviceCreationDescriptor.resources && serviceCreationDescriptor.resources.memorySizeMB
|
||||
serviceCreationDescriptor.resources &&
|
||||
serviceCreationDescriptor.resources.memorySizeMB
|
||||
? serviceCreationDescriptor.resources.memorySizeMB
|
||||
: 1000;
|
||||
|
||||
@@ -134,7 +147,8 @@ export class DockerService {
|
||||
};
|
||||
|
||||
if (serviceCreationDescriptor.resources) {
|
||||
limits.MemoryBytes = serviceCreationDescriptor.resources.memorySizeMB * 1000000;
|
||||
limits.MemoryBytes =
|
||||
serviceCreationDescriptor.resources.memorySizeMB * 1000000;
|
||||
}
|
||||
|
||||
const response = await dockerHost.request('POST', '/services/create', {
|
||||
@@ -177,7 +191,7 @@ export class DockerService {
|
||||
|
||||
const createdService = await DockerService.getServiceByName(
|
||||
dockerHost,
|
||||
serviceCreationDescriptor.name
|
||||
serviceCreationDescriptor.name,
|
||||
);
|
||||
return createdService;
|
||||
}
|
||||
@@ -223,7 +237,10 @@ export class DockerService {
|
||||
}
|
||||
|
||||
public async reReadFromDockerEngine() {
|
||||
const dockerData = await this.dockerHostRef.request('GET', `/services/${this.ID}`);
|
||||
const dockerData = await this.dockerHostRef.request(
|
||||
'GET',
|
||||
`/services/${this.ID}`,
|
||||
);
|
||||
// TODO: Better assign: Object.assign(this, dockerData);
|
||||
}
|
||||
|
||||
@@ -231,14 +248,21 @@ export class DockerService {
|
||||
// TODO: implement digest based update recognition
|
||||
|
||||
await this.reReadFromDockerEngine();
|
||||
const dockerImage = await DockerImage.createFromRegistry(this.dockerHostRef, {
|
||||
creationObject: {
|
||||
imageUrl: this.Spec.TaskTemplate.ContainerSpec.Image,
|
||||
}
|
||||
});
|
||||
const dockerImage = await DockerImage.createFromRegistry(
|
||||
this.dockerHostRef,
|
||||
{
|
||||
creationObject: {
|
||||
imageUrl: this.Spec.TaskTemplate.ContainerSpec.Image,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const imageVersion = new plugins.smartversion.SmartVersion(dockerImage.Labels.version);
|
||||
const serviceVersion = new plugins.smartversion.SmartVersion(this.Spec.Labels.version);
|
||||
const imageVersion = new plugins.smartversion.SmartVersion(
|
||||
dockerImage.Labels.version,
|
||||
);
|
||||
const serviceVersion = new plugins.smartversion.SmartVersion(
|
||||
this.Spec.Labels.version,
|
||||
);
|
||||
if (imageVersion.greaterThan(serviceVersion)) {
|
||||
console.log(`service ${this.Spec.Name} needs to be updated`);
|
||||
return true;
|
||||
|
5
ts/logger.ts
Normal file
5
ts/logger.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { commitinfo } from './00_commitinfo_data.js';
|
||||
|
||||
export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
|
||||
logger.enableConsole();
|
@@ -1,3 +0,0 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export const logger = new plugins.smartlog.ConsoleLog();
|
@@ -2,7 +2,7 @@ import * as plugins from './plugins.js';
|
||||
|
||||
export const packageDir = plugins.path.resolve(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../'
|
||||
'../',
|
||||
);
|
||||
|
||||
export const nogitDir = plugins.path.resolve(packageDir, '.nogit/');
|
||||
|
@@ -5,6 +5,7 @@ export { path };
|
||||
|
||||
// @pushrocks scope
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as smartarchive from '@push.rocks/smartarchive';
|
||||
import * as smartbucket from '@push.rocks/smartbucket';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartjson from '@push.rocks/smartjson';
|
||||
@@ -15,10 +16,12 @@ import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
import * as smartstring from '@push.rocks/smartstring';
|
||||
import * as smartstream from '@push.rocks/smartstream';
|
||||
import * as smartunique from '@push.rocks/smartunique';
|
||||
import * as smartversion from '@push.rocks/smartversion';
|
||||
|
||||
export {
|
||||
lik,
|
||||
smartarchive,
|
||||
smartbucket,
|
||||
smartfile,
|
||||
smartjson,
|
||||
@@ -29,6 +32,7 @@ export {
|
||||
smartrequest,
|
||||
smartstring,
|
||||
smartstream,
|
||||
smartunique,
|
||||
smartversion,
|
||||
};
|
||||
|
||||
|
@@ -6,9 +6,9 @@
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"verbatimModuleSyntax": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {}
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
"exclude": ["dist_*/**/*.d.ts"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user