├── .npmrc ├── requirements.txt ├── .npmignore ├── .gitignore ├── repo.yml ├── .editorconfig ├── .github └── workflows │ └── flowzone.yml ├── tsconfig.json ├── Dockerfile ├── package.json ├── README.md ├── LICENSE ├── src └── preload.py ├── lib └── preload.ts └── CHANGELOG.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sh==1.14.3 2 | retry==0.9.2 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.md 3 | *.log 4 | 5 | doc 6 | example 7 | test 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | 3 | node_modules 4 | package-lock.json 5 | 6 | .idea 7 | /build 8 | -------------------------------------------------------------------------------- /repo.yml: -------------------------------------------------------------------------------- 1 | type: 'node' 2 | upstream: 3 | - repo: 'balena-sdk' 4 | url: 'https://github.com/balena-io/balena-sdk' 5 | - repo: 'docker-progress' 6 | url: 'https://github.com/balena-io-modules/docker-progress' 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # See http://editorconfig.org 4 | root = true 5 | 6 | [*.py] 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.github/workflows/flowzone.yml: -------------------------------------------------------------------------------- 1 | name: Flowzone 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, closed] 6 | branches: 7 | - "main" 8 | - "master" 9 | 10 | jobs: 11 | flowzone: 12 | name: Flowzone 13 | uses: product-os/flowzone/.github/workflows/flowzone.yml@master 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "target": "es2019", 6 | "outDir": "build", 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "preserveConstEnums": true, 12 | "removeComments": true, 13 | "sourceMap": true, 14 | "skipLibCheck": true, 15 | "preserveSymlinks": true, 16 | "allowJs": true, 17 | "checkJs": true 18 | }, 19 | "include": [ 20 | "./lib/**/*", 21 | "./bin/**/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # avoid alpine 3.13 or later due to this issue on armv7 2 | # https://wiki.alpinelinux.org/wiki/Release_Notes_for_Alpine_3.13.0#time64_requirements 3 | FROM alpine:3.21 4 | 5 | WORKDIR /usr/src/app 6 | 7 | # coreutils so we have the real dd, not the busybox one 8 | # hadolint ignore=DL3018 9 | RUN apk add --no-cache curl py3-pip parted btrfs-progs util-linux sfdisk file coreutils sgdisk e2fsprogs-extra docker 10 | 11 | COPY requirements.txt ./ 12 | 13 | RUN pip3 install --no-cache-dir -r requirements.txt --break-system-packages 14 | 15 | COPY src/ ./ 16 | 17 | CMD ["python3", "/usr/src/app/preload.py"] 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "balena-preload", 3 | "version": "18.0.5", 4 | "description": "Preload balena OS images with a user application container", 5 | "license": "Apache-2.0", 6 | "author": "Balena Ltd (https://balena.io)", 7 | "main": "build/preload.js", 8 | "types": "build/preload.d.ts", 9 | "engines": { 10 | "node": "^20.12.0 || >=22.0.0" 11 | }, 12 | "keywords": [ 13 | "balena", 14 | "balenaos", 15 | "image", 16 | "docker", 17 | "container" 18 | ], 19 | "files": [ 20 | "build/", 21 | "src/preload.py", 22 | "Dockerfile", 23 | "requirements.txt" 24 | ], 25 | "dependencies": { 26 | "balena-sdk": "^22.0.0", 27 | "compare-versions": "^3.6.0", 28 | "docker-progress": "^5.0.0", 29 | "dockerode": "^4.0.2", 30 | "get-port": "^3.2.0", 31 | "lodash": "^4.17.21", 32 | "node-cleanup": "^2.1.2", 33 | "tar-fs": "^2.1.1" 34 | }, 35 | "devDependencies": { 36 | "@balena/lint": "^7.2.6", 37 | "@types/dockerode": "^3.3.23", 38 | "@types/node": "^20.17.22", 39 | "@types/request-promise": "^4.1.48", 40 | "@types/tar-fs": "^2.0.1", 41 | "catch-uncommitted": "^2.0.0", 42 | "typescript": "^5.6.2" 43 | }, 44 | "homepage": "https://github.com/balena-io/balena-preload", 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/balena-io/balena-preload.git" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/balena-io/balena-preload/issues" 51 | }, 52 | "scripts": { 53 | "lint": "balena-lint --fix lib", 54 | "lint-python": "flake8 src/preload.py", 55 | "test": "tsc --noEmit && npm run lint && catch-uncommitted --skip-node-versionbot-changes", 56 | "prepare": "tsc" 57 | }, 58 | "versionist": { 59 | "publishedAt": "2025-08-07T16:47:59.931Z" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # balena-preload 2 | [![npm](https://img.shields.io/npm/v/balena-preload.svg?style=flat-square)](https://npmjs.com/package/balena-preload) 3 | [![npm license](https://img.shields.io/npm/l/balena-preload.svg?style=flat-square)](https://npmjs.com/package/balena-preload) 4 | [![npm downloads](https://img.shields.io/npm/dm/balena-preload.svg?style=flat-square)](https://npmjs.com/package/balena-preload) 5 | 6 | Script for preloading balena OS images (`.img`) with a user application container. 7 | 8 | Using this will allow images with supervisor version above 1.0.0 to run the user application without connectivity, and without the need to download the container. 9 | 10 | ## Warning 11 | 12 | In order to preload images that use the overlay2 Docker storage driver (like 13 | nvidia jetson tx2 for example), you need to load the `overlay` Linux module: 14 | 15 | ```sh 16 | sudo modprobe overlay 17 | ``` 18 | 19 | For other images you will need to have the `aufs` module loaded. 20 | 21 | 22 | ## Deprecation 23 | 24 | The standalone mode described below (balena-preload) is now deprecated. 25 | It will be removed in a future release. 26 | You should use [balena-cli](https://www.npmjs.com/package/balena-cli) instead. 27 | 28 | Install [balena-cli](https://www.npmjs.com/package/balena-cli) and run 29 | `balena help preload`. 30 | 31 | 32 | ## Install via [npm](https://npmjs.com) 33 | 34 | ```sh 35 | $ npm install --global balena-preload 36 | ``` 37 | 38 | 39 | 40 | - [Requirements](#requirements) 41 | - [Known Issues](#known-issues) 42 | - [Speed Issues For Flasher Images on macOS](#speed-issues-for-flasher-images-on-macos) 43 | - [Version Compatibility](#version-compatibility) 44 | - [BTRFS Support](#btrfs-support) 45 | 46 | 47 | 48 | ## Requirements 49 | 50 | - [Node](https://nodejs.org) 51 | - [Docker](https://www.docker.com) tested on 1.12.6 and up but 17.04 or up is recommended especially on macOS, [docker-toolbox](https://www.docker.com/products/docker-toolbox) is not supported. 52 | Older versions of balena-preload do support docker-toolbox, see [Version Compatibility](#version-compatibility) below. 53 | 54 | ### Issues 55 | 56 | If you encounter any problem, you can [open an issue](https://github.com/balena-io/balena-preload/issues) 57 | 58 | ## Known Issues 59 | 60 | ### Speed Issues For Flasher Images on macOS 61 | 62 | Docker on macOS has [some speed issues with volumes](https://github.com/docker/for-mac/issues/77). 63 | This makes this script slow, especially with Flasher Images. 64 | 65 | ### Version Compatibility 66 | 67 | This version will only work for balena OS versions 1.2 and later. 68 | For versions earlier than 1.2 you will need to checkout commit `5d6d4607bffc98acdf649ce5328e2079dfb9c3d9` of this repo and then follow the steps below. 69 | 70 | ### BTRFS Support 71 | 72 | Since Docker for Mac removed support for the BTRFS storage driver (see [docker/for-mac/issues/388](https://github.com/docker/for-mac/issues/388)), preloading images prior to balena OS 2.0 will require the older [Docker toolbox](https://docs.docker.com/toolbox/toolbox_install_mac/) setup with [VirtualBox](https://www.virtualbox.org/) to function properly. 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/preload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | import json 3 | import os 4 | import re 5 | import sys 6 | import traceback 7 | 8 | from contextlib import contextmanager 9 | from functools import partial 10 | from logging import getLogger, INFO, StreamHandler 11 | from math import ceil, floor 12 | from re import match, search 13 | from retry.api import retry_call 14 | from sh import ( 15 | btrfs, 16 | dd, 17 | df, 18 | docker as _docker, 19 | dockerd, 20 | file, 21 | fsck, 22 | losetup, 23 | mount, 24 | parted, 25 | resize2fs, 26 | sfdisk, 27 | sgdisk, 28 | umount, 29 | update_ca_certificates, 30 | ErrorReturnCode, 31 | ) 32 | from shutil import copyfile, rmtree 33 | from tempfile import mkdtemp, NamedTemporaryFile 34 | 35 | os.environ["LANG"] = "C" 36 | 37 | IMAGE = "/img/balena.img" 38 | 39 | # In bytes: 40 | SECTOR_SIZE = 512 41 | MBR_SIZE = 512 42 | GPT_SIZE = SECTOR_SIZE * 34 43 | MBR_BOOTSTRAP_CODE_SIZE = 446 44 | 45 | SPLASH_IMAGE_FROM = "/img/balena-logo.png" 46 | 47 | CONFIG_PARTITIONS = [ 48 | "resin-boot", # resinOS 1.26+ 49 | "resin-conf", # resinOS 1.8 50 | "flash-conf", # resinOS 1.8 flash 51 | "flash-boot", # flasher images 52 | ] 53 | 54 | # command: 55 | # balena images --all --format '{{.Repository}} {{.Tag}}' 56 | # matches: 57 | # balena_supervisor v12.11.38 58 | # balena/aarch64-supervisor v11.14.0 59 | SUPERVISOR_REPOSITORY_RE = "^(((balena|resin|balenaplayground)/)?(armel|rpi|armv7hf|aarch64|i386|amd64|i386-nlp)-supervisor|balena_supervisor)$" 60 | 61 | # 'sh' module '_truncate_exc' option: 62 | # http://amoffat.github.io/sh/sections/special_arguments.html#truncate-exc 63 | # https://github.com/amoffat/sh/blob/1.12.14/sh.py#L1151 64 | SH_OPTS = {"_truncate_exc": False} 65 | 66 | DOCKER_HOST = "tcp://0.0.0.0:{}".format(os.environ.get("DOCKER_PORT") or 8000) 67 | docker = partial(_docker, "--host", DOCKER_HOST, **SH_OPTS) 68 | DOCKER_TLS = "false" 69 | 70 | log = getLogger(__name__) 71 | log.setLevel(INFO) 72 | log.addHandler(StreamHandler()) 73 | 74 | PARTITIONS_CACHE = {} 75 | 76 | 77 | class RetryCounter: 78 | """Counter and logger for the number of times that a function is retried. 79 | Usage: 80 | retry_counter = RetryCounter() 81 | hint = "Try reducing the system entropy" 82 | wrapped_func, wrap_key = retry_counter.wrap(my_func, hint, *args) 83 | retry_call(wrapped_func, fargs=args, ...) 84 | retry_counter.clear(wrap_key) 85 | """ 86 | 87 | def __init__(self): 88 | self.counter = {} 89 | 90 | @staticmethod 91 | def key(func_name, *args, **kwargs): 92 | return " ".join( 93 | str(e) for e in (func_name,) + args + tuple(kwargs.values()) 94 | ) 95 | 96 | def clear(self, key): 97 | del self.counter[key] 98 | 99 | def inc(self, key): 100 | self.counter[key] = self.counter.setdefault(key, 0) + 1 101 | return self.counter[key] 102 | 103 | def wrap(self, func, hint, *args, **kwargs): 104 | """Return a function that wraps the given func, counting its usage""" 105 | key = self.key(func.__name__, *args, **kwargs) 106 | 107 | def wrapped(*args, **kwargs): 108 | count = self.inc(key) 109 | if count > 1: 110 | log.info( 111 | "\nRetrying (count={}) {}\n{}".format( 112 | count, 113 | key, 114 | hint, 115 | ), 116 | ) 117 | return func(*args, **kwargs) 118 | 119 | return (wrapped, key) 120 | 121 | 122 | retry_counter = RetryCounter() 123 | 124 | 125 | def get_partitions(image): 126 | return {p.label: p for p in PartitionTable(image).partitions if p.label} 127 | 128 | 129 | def prepare_global_partitions(): 130 | partitions = os.environ.get("PARTITIONS") 131 | if partitions is not None: 132 | partitions = json.loads(partitions) 133 | result = {} 134 | for label, data in partitions.items(): 135 | result[label] = FilePartition(data["image"], label) 136 | return result 137 | return get_partitions(IMAGE) 138 | 139 | 140 | @contextmanager 141 | def losetup_context_manager(image, offset=None, size=None): 142 | args = ["-f", "--show"] 143 | if offset is not None: 144 | args.extend(["--offset", offset]) 145 | if size is not None: 146 | args.extend(["--sizelimit", size]) 147 | args.append(image) 148 | hint = """\ 149 | Hint: If using a Virtual Machine, consider increasing the number of processors. 150 | If using Docker Desktop for Windows or macOS, it may require restarting.""" 151 | lo_wrap, lo_wrap_key = retry_counter.wrap(losetup, hint, *args, **SH_OPTS) 152 | # In the case of slow hardware the kernel might be in the middle of 153 | # tearing down internal structure 154 | device = retry_call( 155 | lo_wrap, 156 | fargs=args, 157 | tries=10, 158 | delay=3, 159 | max_delay=30, 160 | backoff=2 161 | ).stdout.decode("utf8").strip() 162 | retry_counter.clear(lo_wrap_key) 163 | yield device 164 | losetup("-d", device, **SH_OPTS) 165 | 166 | 167 | @contextmanager 168 | def device_mount_context_manager(device): 169 | mountpoint = mkdtemp() 170 | mount(device, mountpoint, **SH_OPTS) 171 | yield mountpoint 172 | umount(mountpoint, **SH_OPTS) 173 | os.rmdir(mountpoint) 174 | 175 | 176 | @contextmanager 177 | def mount_context_manager(image, offset=None, size=None): 178 | with losetup_context_manager(image, offset, size) as device: 179 | with device_mount_context_manager(device) as mountpoint: 180 | yield mountpoint 181 | 182 | 183 | class FilePartition(object): 184 | def __init__(self, image, label): 185 | self.image = image 186 | self.label = label 187 | 188 | def losetup_context_manager(self): 189 | return losetup_context_manager(self.image) 190 | 191 | def mount_context_manager(self): 192 | return mount_context_manager(self.image) 193 | 194 | def resize(self, additional_bytes): 195 | if additional_bytes > 0: 196 | expand_file(self.image, additional_bytes) 197 | expand_filesystem(self) 198 | 199 | def str(self): 200 | return self.image 201 | 202 | def free_space(self): 203 | with self.losetup_context_manager() as device: 204 | fs = get_filesystem(device) 205 | with mount_context_manager(device) as mountpoint: 206 | if fs == 'btrfs': 207 | out = btrfs("fi", "usage", "--raw", mountpoint, **SH_OPTS) 208 | for line in out: 209 | line = line.strip() 210 | if line.startswith("Free (estimated):"): 211 | return int(line[line.rfind(" ") + 1:-1]) 212 | else: 213 | output = df("-B1", "--output=avail", mountpoint, **SH_OPTS) 214 | return int(output.split("\n")[1].strip()) 215 | 216 | 217 | class Partition(FilePartition): 218 | def __init__( 219 | self, 220 | partition_table, 221 | number, 222 | node=None, 223 | start=None, 224 | size=None, 225 | type=None, 226 | uuid=None, 227 | name=None, 228 | bootable=False, 229 | ): 230 | self.partition_table = partition_table 231 | self.number = number 232 | self.parent = None 233 | self.node = node 234 | self.start = start 235 | self.size = size 236 | self.type = type 237 | self.uuid = uuid 238 | self.name = name 239 | self.bootable = bootable 240 | # label, not part of the sfdisk script 241 | self.label = self._get_label() 242 | 243 | def _get_label(self): 244 | with self.losetup_context_manager() as device: 245 | out = file("-s", device, **SH_OPTS).stdout.decode("utf8").strip() 246 | # "label:" is for fat partitions, 247 | # "volume name" is for ext partitions 248 | # "BTRFS Filesystem label" is for btrfs partitions 249 | match = search( 250 | '(BTRFS Filesystem label|label:|volume name) "(.*)"', 251 | out, 252 | ) 253 | if match is not None: 254 | return match.groups()[1].strip() 255 | 256 | def set_parent(self, parent): 257 | # For logical partitions on MBR disks we store the parent extended 258 | # partition 259 | assert self.partition_table.label == "dos" 260 | self.parent = parent 261 | 262 | @property 263 | def image(self): 264 | return self.partition_table.image 265 | 266 | @property 267 | def end(self): 268 | # last sector (included) 269 | return self.start + self.size - 1 270 | 271 | @property 272 | def start_bytes(self): 273 | return self.start * SECTOR_SIZE 274 | 275 | @property 276 | def size_bytes(self): 277 | return self.size * SECTOR_SIZE 278 | 279 | @property 280 | def end_bytes(self): 281 | # last byte (included) 282 | return self.start_bytes + self.size_bytes - 1 283 | 284 | def is_included_in(self, other): 285 | return ( 286 | other.start <= self.start <= other.end and 287 | other.start <= self.end <= other.end 288 | ) 289 | 290 | def is_extended(self): 291 | return self.partition_table.label == "dos" and self.type == "f" 292 | 293 | def is_last(self): 294 | # returns True if this partition is the last on the disk 295 | return self == self.partition_table.get_partitions_in_disk_order()[-1] 296 | 297 | def get_sfdisk_line(self): 298 | result = "{} : start={}, size={}, type={}".format( 299 | self.node, 300 | self.start, 301 | self.size, 302 | self.type 303 | ) 304 | if self.uuid is not None: 305 | result += ", uuid={}".format(self.uuid) 306 | if self.name is not None: 307 | result += ', name="{}"'.format(self.name) 308 | if self.bootable: 309 | result += ", bootable" 310 | return result 311 | 312 | def losetup_context_manager(self): 313 | return losetup_context_manager( 314 | self.image, 315 | self.start_bytes, 316 | self.size_bytes, 317 | ) 318 | 319 | def mount_context_manager(self): 320 | return mount_context_manager( 321 | self.image, 322 | self.start_bytes, 323 | self.size_bytes, 324 | ) 325 | 326 | def str(self): 327 | return "partition n°{} of {}".format(self.number, self.image) 328 | 329 | def _resize_last_partition_of_disk_image(self, additional_bytes): 330 | # This is the simple case: expand the partition and its parent extended 331 | # partition if it is a logical one. 332 | additional_sectors = additional_bytes // SECTOR_SIZE 333 | # Expand image size 334 | expand_file(self.image, additional_bytes) 335 | if self.partition_table.label == "gpt": 336 | # Move backup GPT data structures to the end of the disk. 337 | # This is required because we resized the image. 338 | sgdisk("-e", self.image, **SH_OPTS) 339 | parted_args = [self.image] 340 | if self.parent is not None: 341 | log.info("Expanding extended {}".format(self.parent.str())) 342 | # Resize the extended partition 343 | parted_args.extend(["resizepart", self.parent.number, "100%"]) 344 | self.parent.size += additional_sectors 345 | # Resize the partition itself 346 | log.info( 347 | "Expanding{} {}".format( 348 | " logical" if self.parent is not None else "", 349 | self.str(), 350 | ) 351 | ) 352 | parted_args.extend(["resizepart", self.number, "100%"]) 353 | parted(*parted_args, _in="fix\n", **SH_OPTS) 354 | self.size += additional_sectors 355 | 356 | def _resize_partition_on_disk_image(self, additional_bytes): 357 | # This function expects the partitions to be in disk order: it will 358 | # fail if there are primary partitions after an extended one containing 359 | # logical partitions. 360 | # Resizing logical partitions that are not the last on the disk is not 361 | # implemented 362 | assert self.parent is None 363 | partition_table = self.partition_table 364 | image = self.image 365 | # Create a new temporary file of the correct size 366 | tmp = NamedTemporaryFile(dir=os.path.dirname(image), delete=False) 367 | tmp.truncate(file_size(image) + additional_bytes) 368 | tmp.close() 369 | # Update the partition table 370 | additional_sectors = additional_bytes // SECTOR_SIZE 371 | # resize the partition 372 | self.size += additional_sectors 373 | # move the partitions after 374 | for part in partition_table.partitions[self.number:]: 375 | part.start += additional_sectors 376 | # update last lba 377 | if partition_table.lastlba is not None: 378 | partition_table.lastlba += additional_sectors 379 | sfdisk(tmp.name, _in=partition_table.get_sfdisk_script(), **SH_OPTS) 380 | # Now we copy the data from the image to the temporary file 381 | copy = partial( 382 | ddd, 383 | _if=image, 384 | of=tmp.name, 385 | bs=1024 ** 2, # one MiB 386 | conv="notrunc", 387 | iflag="count_bytes,skip_bytes", # count and skip in bytes 388 | oflag="seek_bytes", # seek in bytes 389 | ) 390 | # Preserve GRUB 391 | copy(count=MBR_BOOTSTRAP_CODE_SIZE) 392 | # Copy across any data that's located between the MBR and the first 393 | # partition (some devices rely on the bootloader being there, like the 394 | # Variscite DART-6UL) 395 | if self.partition_table.label == "dos": 396 | copy( 397 | skip=MBR_SIZE, 398 | seek=MBR_SIZE, 399 | count=partition_table.partitions[0].start_bytes - MBR_SIZE, 400 | ) 401 | elif self.partition_table.label == "gpt": 402 | copy( 403 | skip=GPT_SIZE, 404 | seek=GPT_SIZE, 405 | count=partition_table.partitions[0].start_bytes - GPT_SIZE, 406 | ) 407 | # TODO: if we copy an extended partition, there is no need to copy its 408 | # logical partitions. 409 | # Copy partitions before and the partition itself 410 | for part in partition_table.partitions[:self.number]: 411 | # No need to copy extended partitions, we'll copy their logical 412 | # partitions 413 | if not part.is_extended(): 414 | copy( 415 | skip=part.start_bytes, 416 | seek=part.start_bytes, 417 | count=part.size_bytes, 418 | ) 419 | # Copy partitions after. 420 | for part in partition_table.partitions[self.number:]: 421 | if not part.is_extended(): 422 | copy( 423 | skip=part.start_bytes, 424 | seek=part.start_bytes + additional_bytes, 425 | count=part.size_bytes, 426 | ) 427 | # Replace the original image contents. 428 | ddd(_if=tmp.name, of=image, bs=1024 ** 2) 429 | 430 | def resize(self, additional_bytes): 431 | if additional_bytes > 0: 432 | # Is it the last partition on the disk? 433 | if self.is_last(): 434 | self._resize_last_partition_of_disk_image(additional_bytes) 435 | else: 436 | self._resize_partition_on_disk_image(additional_bytes) 437 | expand_filesystem(self) 438 | 439 | 440 | class PartitionTable(object): 441 | def __init__(self, image): 442 | self.image = image 443 | data = json.loads( 444 | sfdisk("--json", image, **SH_OPTS).stdout.decode("utf8") 445 | )["partitiontable"] 446 | self.label = data["label"] 447 | assert self.label in ("dos", "gpt") 448 | self.id = data["id"] 449 | self.device = data["device"] 450 | self.unit = data["unit"] 451 | self.firstlba = data.get("firstlba") 452 | self.lastlba = data.get("lastlba") 453 | self.partitions = [] 454 | extended_partition = None 455 | for number, partition_data in enumerate(data["partitions"], 1): 456 | part = Partition(self, number, **partition_data) 457 | if part.is_extended(): 458 | extended_partition = part 459 | if extended_partition and part.is_included_in(extended_partition): 460 | part.set_parent(extended_partition) 461 | self.partitions.append(part) 462 | 463 | def get_partitions_in_disk_order(self): 464 | # Returns the partitions in the same order that they are on the disk 465 | # This excludes extended partitions. 466 | partitions = (p for p in self.partitions if not p.is_extended()) 467 | return sorted(partitions, key=lambda p: p.start) 468 | 469 | def get_sfdisk_script(self): 470 | result = ( 471 | "label: {}\n" 472 | "label-id: {}\n" 473 | "device: {}\n" 474 | "unit: {}\n" 475 | ).format(self.label, self.id, self.device, self.unit) 476 | if self.firstlba is not None: 477 | result += "first-lba: {}\n".format(self.firstlba) 478 | if self.lastlba is not None: 479 | result += "last-lba: {}\n".format(self.lastlba) 480 | result += "\n" 481 | result += "\n".join(p.get_sfdisk_line() for p in self.partitions) 482 | return result 483 | 484 | 485 | def get_filesystem(device): 486 | result = fsck("-N", device, **SH_OPTS) 487 | line = result.stdout.decode("utf8").strip().split("\n")[1] 488 | return line.rsplit(" ", 2)[-2].split(".")[1] 489 | 490 | 491 | def expand_filesystem(partition): 492 | with partition.losetup_context_manager() as device: 493 | # Detects the partition filesystem (ext{2,3,4} or btrfs) and uses the 494 | # appropriate tool to expand the filesystem to all the available space. 495 | fs = get_filesystem(device) 496 | log.info( 497 | "Resizing {} filesystem of {} using {}".format( 498 | fs, 499 | partition.str(), 500 | device, 501 | ) 502 | ) 503 | if fs.startswith("ext"): 504 | try: 505 | kwargs = {"_ok_code": [0, 1, 2], **SH_OPTS} 506 | status = fsck("-p", "-f", device, **kwargs) 507 | if status.exit_code == 0: 508 | log.info("File system OK") 509 | else: 510 | log.warning("File system errors corrected") 511 | except ErrorReturnCode: 512 | raise Exception("File system errors could not be corrected") 513 | resize2fs("-f", device, **SH_OPTS) 514 | elif fs == "btrfs": 515 | # For btrfs we need to mount the fs for resizing. 516 | with mount_context_manager(device) as mountpoint: 517 | btrfs("filesystem", "resize", "max", mountpoint, **SH_OPTS) 518 | 519 | 520 | def expand_file(path, additional_bytes): 521 | with open(path, "a") as f: 522 | size = f.tell() 523 | f.truncate(size + additional_bytes) 524 | 525 | 526 | def fix_rce_docker(mountpoint): 527 | """ 528 | Removes the /rce folder if a /docker folder exists. 529 | Returns "/docker" if this folder exists, "/rce" 530 | otherwise. 531 | """ 532 | _docker_dir = mountpoint + "/docker" 533 | _rce_dir = mountpoint + "/rce" 534 | if os.path.isdir(_docker_dir): 535 | if os.path.isdir(_rce_dir): 536 | rmtree(_rce_dir) 537 | return _docker_dir 538 | else: 539 | return _rce_dir 540 | 541 | 542 | def start_docker_daemon(storage_driver, docker_dir): 543 | """Starts the docker daemon and waits for it to be ready.""" 544 | running_dockerd = dockerd( 545 | storage_driver=storage_driver, 546 | data_root=docker_dir, 547 | tls=DOCKER_TLS, 548 | host=DOCKER_HOST, 549 | _bg=True, 550 | **SH_OPTS, 551 | ) 552 | log.info("Waiting for Docker to start...") 553 | ok = False 554 | while not ok: 555 | # dockerd should not exit, if it does, we'll throw an exception. 556 | if running_dockerd.process.exit_code is not None: 557 | # There is no reason for dockerd to exit with a 0 status now. 558 | assert running_dockerd.process.exit_code != 0 559 | # This will raise an sh.ErrorReturnCode_X exception. 560 | running_dockerd.wait() 561 | # Check that we can connect to dockerd. 562 | output = docker("version", _ok_code=[0, 1]) 563 | ok = output.exit_code == 0 564 | log.info("Docker started") 565 | return running_dockerd 566 | 567 | 568 | def read_file(name): 569 | with open(name, "rb") as f: 570 | return f.read() 571 | 572 | 573 | def write_file(name, content): 574 | with open(name, "wb") as f: 575 | f.write(content) 576 | 577 | 578 | @contextmanager 579 | def docker_context_manager(storage_driver, mountpoint): 580 | docker_dir = fix_rce_docker(mountpoint) 581 | # If we don't remove //network/files/local-kv.db and the 582 | # preload container was started with bridged networking, the following 583 | # dockerd is not reachable from the host. 584 | local_kv_db_path = "{}/network/files/local-kv.db".format(docker_dir) 585 | kv_file_existed = ( 586 | os.path.exists(local_kv_db_path) and os.path.isfile(local_kv_db_path) 587 | ) 588 | if kv_file_existed: 589 | local_kv_db_content = read_file(local_kv_db_path) 590 | os.remove(local_kv_db_path) 591 | running_dockerd = start_docker_daemon(storage_driver, docker_dir) 592 | yield 593 | running_dockerd.terminate() 594 | running_dockerd.wait() 595 | if kv_file_existed: 596 | write_file(local_kv_db_path, local_kv_db_content) 597 | 598 | 599 | def write_resin_device_pinning(app_data, output): 600 | """Create resin-device-pinnnig.json to hold pinning information""" 601 | if type(app_data) != dict: 602 | # app_data is a list when the supervisor version is < 7.0.0, 603 | # pinning is not suported on these. 604 | return 605 | if not app_data.get("pinDevice", False): 606 | return 607 | apps = app_data.get("apps", {}) 608 | if len(apps) != 1: 609 | raise Exception("Malformed apps.json") 610 | 611 | with open(output, "w") as f: 612 | f.write( 613 | "RELEASE_ID={}".format( 614 | next(iter(apps.values())).get('releaseId'), 615 | ), 616 | ) 617 | 618 | 619 | def write_apps_json(data, output): 620 | """Writes data dict to output as json""" 621 | with open(output, "w") as f: 622 | json.dump(data, f, indent=4, sort_keys=True) 623 | 624 | 625 | def replace_splash_image(splash_image_path, image=None): 626 | """ 627 | Replaces the balena logo used on boot splash to allow a more branded 628 | experience. 629 | """ 630 | if os.path.isfile(SPLASH_IMAGE_FROM): 631 | boot = ( 632 | get_partition("flash-boot") or 633 | get_partition("resin-boot", image) 634 | ) 635 | with boot.mount_context_manager() as mpoint: 636 | path = mpoint + splash_image_path 637 | if os.path.isdir(os.path.dirname(path)): 638 | log.info("Replacing splash image") 639 | copyfile(SPLASH_IMAGE_FROM, path) 640 | else: 641 | log.info( 642 | "No splash folder on the boot partition, the splash image " 643 | "won't be inserted." 644 | ) 645 | else: 646 | log.info("Leaving splash image alone") 647 | 648 | 649 | def start_dockerd_and_wait_for_stdin(app_data, image=None): 650 | rootA_file_contents = get_rootA_file_contents(image) 651 | driver = get_docker_storage_driver( 652 | rootA_file_contents.get("docker_service", ""), 653 | ) 654 | part = get_partition("resin-data", image) 655 | with part.mount_context_manager() as mpoint: 656 | write_apps_json(app_data, mpoint + "/apps.json") 657 | with docker_context_manager(driver, mpoint): 658 | # Signal that Docker is ready. 659 | print(json.dumps({"statusCode": 0})) 660 | sys.stdout.flush() 661 | # Wait for the js to finish its job. 662 | input() 663 | 664 | 665 | def round_to_sector_size(size, sector_size=SECTOR_SIZE): 666 | sectors = size / sector_size 667 | if not sectors.is_integer(): 668 | sectors = floor(sectors) + 1 669 | return int(sectors * sector_size) 670 | 671 | 672 | def file_size(path): 673 | with open(path, "a") as f: 674 | return f.tell() 675 | 676 | 677 | def ddd(**kwargs): 678 | # dd helper 679 | return dd( 680 | *("{}={}".format(k.lstrip("_"), v) for k, v in kwargs.items()), 681 | **SH_OPTS, 682 | ) 683 | 684 | 685 | def get_json(partition_name, path, image=None): 686 | part = get_partition(partition_name, image) 687 | if part: 688 | with part.mount_context_manager() as mountpoint: 689 | try: 690 | with open(os.path.join(mountpoint, path)) as f: 691 | return json.load(f) 692 | except FileNotFoundError: 693 | pass 694 | 695 | 696 | def get_device_type(image=None): 697 | result = get_json("resin-boot", "device-type.json", image=image) 698 | if result is None: 699 | result = get_json("flash-boot", "device-type.json", image=image) 700 | return result 701 | 702 | 703 | def get_config(image=None): 704 | for partition_name in CONFIG_PARTITIONS: 705 | data = get_json(partition_name, "config.json", image=image) 706 | if data is not None: 707 | return data 708 | 709 | 710 | def preload(additional_bytes, app_data, splash_image_path, image=None): 711 | replace_splash_image(splash_image_path, image) 712 | part = get_partition("resin-data", image) 713 | part.resize(additional_bytes) 714 | start_dockerd_and_wait_for_stdin(app_data, image) 715 | 716 | 717 | def get_inner_image_path(root_mountpoint): 718 | opt = os.path.join(root_mountpoint, "opt") 719 | device_type = get_device_type() 720 | if device_type is not None: 721 | deploy_artifact = device_type["yocto"]["deployArtifact"] 722 | return os.path.join(opt, deploy_artifact.replace("flasher-", "", 1)) 723 | else: 724 | # Old images don't have a device type file (resinOS 1.8), 725 | # return the first file in "/opt". 726 | return next( 727 | filter( 728 | os.path.isfile, 729 | map(partial(os.path.join, opt), os.listdir(opt)) 730 | ) 731 | ) 732 | 733 | 734 | def _get_balena_os_version(etc_issue_contents): 735 | """ 736 | Return a balenaOS version string such as "2.53.0", given the contents 737 | of the "/etc/issue" file in the etc_issue_contents argument. 738 | """ 739 | m = match('balenaOS (.+?) ', etc_issue_contents) 740 | return m[1] if m is not None else "" 741 | 742 | 743 | def _get_images_and_supervisor_version(image=None): 744 | rootA_file_contents = get_rootA_file_contents(image) 745 | driver = get_docker_storage_driver( 746 | rootA_file_contents.get("docker_service", ""), 747 | ) 748 | part = get_partition("resin-data", image) 749 | with part.mount_context_manager() as mountpoint: 750 | with docker_context_manager(driver, mountpoint): 751 | output = docker( 752 | "images", 753 | "--all", 754 | "--format", 755 | "{{.Repository}} {{.Tag}}" 756 | ) 757 | images = set() 758 | supervisor_version = None 759 | for line in output: 760 | repository, version = line.strip().split() 761 | if match(SUPERVISOR_REPOSITORY_RE, repository): 762 | if version != "latest": 763 | version_search = re.search( 764 | r"^v?(?P\d+\.\d+\.\d+).*", 765 | version, 766 | ) 767 | if version_search: 768 | supervisor_version = version_search.group('semver') 769 | else: 770 | raise Exception( 771 | "Could not extract supervisor version.", 772 | ) 773 | else: 774 | images.add(repository) 775 | return ( 776 | list(images), 777 | supervisor_version, 778 | _get_balena_os_version( 779 | rootA_file_contents.get("/etc/issue", ""), 780 | ), 781 | ) 782 | 783 | 784 | def get_images_and_supervisor_version(): 785 | flasher_root = get_partition("flash-rootA") 786 | if flasher_root: 787 | with flasher_root.mount_context_manager() as mountpoint: 788 | inner_image_path = get_inner_image_path(mountpoint) 789 | return _get_images_and_supervisor_version(inner_image_path) 790 | return _get_images_and_supervisor_version() 791 | 792 | 793 | def free_space(): 794 | flasher_root = get_partition("flash-rootA") 795 | if flasher_root: 796 | with flasher_root.mount_context_manager() as mountpoint: 797 | inner_image_path = get_inner_image_path(mountpoint) 798 | return get_partition("resin-data", inner_image_path).free_space() 799 | return get_partition("resin-data").free_space() 800 | 801 | 802 | def is_non_empty_folder(folder): 803 | # True if the folder has at least one file not starting with a dot. 804 | if not os.path.exists(folder) or not os.path.isdir(folder): 805 | return False 806 | return any(f for f in os.listdir(folder) if not f.startswith(".")) 807 | 808 | 809 | def find_non_empty_folder_in_path(path, child_dir=""): 810 | # If child_dir is not given, returns any non empty folder like /...; 811 | # else, returns any non empty folder like /.../ 812 | # where ... can be any subfodler of . 813 | if os.path.exists(path) and os.path.isdir(path): 814 | for folder in os.listdir(path): 815 | folder_path = os.path.join(path, folder, child_dir) 816 | if is_non_empty_folder(folder_path): 817 | return folder_path 818 | 819 | 820 | def find_docker_aufs_root(mountpoint): 821 | # We're looking for a //aufs/diff// folder 822 | # with some files not starting with a '.' 823 | for name in ("docker", "balena"): 824 | path = os.path.join(mountpoint, name, "aufs", "diff") 825 | if os.path.isdir(path): 826 | return find_non_empty_folder_in_path(path) 827 | 828 | 829 | def find_docker_overlay2_root(mountpoint): 830 | # We're looking for a //overlay2//diff 831 | # folder with some files not starting with a '.' 832 | for name in ("docker", "balena"): 833 | path = os.path.join(mountpoint, name, "overlay2") 834 | if os.path.isdir(path): 835 | return find_non_empty_folder_in_path(path, "diff") 836 | 837 | 838 | def get_docker_service_file_path(folder): 839 | for name in ("docker", "balena"): 840 | fpath = os.path.join( 841 | folder, 842 | "lib", 843 | "systemd", 844 | "system", 845 | name + ".service", 846 | ) 847 | if os.path.exists(fpath): 848 | return fpath 849 | 850 | 851 | def get_rootA_file_contents(image=None): 852 | file_contents = { 853 | "docker_service": "", 854 | "/etc/issue": "", 855 | } 856 | part = get_partition("resin-rootA", image) 857 | with part.mount_context_manager() as mountpoint: 858 | docker_root = find_docker_aufs_root(mountpoint) 859 | if docker_root is None: 860 | docker_root = find_docker_overlay2_root(mountpoint) 861 | root_folder = docker_root if docker_root is not None else mountpoint 862 | docker_service_path = get_docker_service_file_path(root_folder) or "" 863 | etc_issue_path = os.path.join(root_folder, "etc", "issue") 864 | with open(docker_service_path) as f: 865 | file_contents["docker_service"] = f.read() 866 | try: 867 | with open(etc_issue_path) as f: 868 | file_contents["/etc/issue"] = f.read() 869 | except OSError: 870 | # If very old or custom images don't have an '/etc/issue' file, 871 | # simply return an empty string for it. 872 | pass 873 | 874 | return file_contents 875 | 876 | 877 | def find_one_of(lst, *args): 878 | try: 879 | for elem in args: 880 | index = lst.index(elem) 881 | return index 882 | except: 883 | return -1 884 | 885 | 886 | def get_docker_storage_driver(docker_service_file_contents): 887 | for line in docker_service_file_contents.strip().split("\n"): 888 | if line.startswith("ExecStart="): 889 | words = line.split() 890 | position = find_one_of(words, "-s", "--storage-driver") 891 | if position != -1 and position < len(words) - 1: 892 | return words[position + 1] 893 | if line.startswith("Environment=BALENAD_STORAGEDRIVER="): 894 | return line.split('=')[-1] 895 | assert False, "Docker storage driver could not be found" 896 | 897 | 898 | def main_preload(app_data, additional_bytes, splash_image_path): 899 | init() 900 | additional_bytes = round_to_sector_size(ceil(additional_bytes)) 901 | flasher_root = get_partition("flash-rootA") 902 | if flasher_root: 903 | flasher_root.resize(additional_bytes) 904 | with flasher_root.mount_context_manager() as mountpoint: 905 | write_resin_device_pinning( 906 | app_data, 907 | mountpoint + "/etc/resin-device-pinning.conf" 908 | ) 909 | inner_image_path = get_inner_image_path(mountpoint) 910 | log.info( 911 | "This is a flasher image, preloading to /{} on {}".format( 912 | inner_image_path.split("/", 2)[2], 913 | flasher_root.str(), 914 | ) 915 | ) 916 | preload( 917 | additional_bytes, 918 | app_data, 919 | splash_image_path, 920 | inner_image_path, 921 | ) 922 | else: 923 | preload(additional_bytes, app_data, splash_image_path) 924 | 925 | 926 | def get_image_info(): 927 | init() 928 | images, supervisor_version, balena_os_version = ( 929 | get_images_and_supervisor_version() 930 | ) 931 | return { 932 | "preloaded_builds": images, 933 | "supervisor_version": supervisor_version, 934 | "free_space": free_space(), 935 | "config": get_config(), 936 | # balena_os_version will be "" if "balenaOS" not found in /etc/issue 937 | "balena_os_version": balena_os_version, 938 | } 939 | 940 | is_initialized = False 941 | 942 | def init(): 943 | global is_initialized 944 | if not is_initialized: 945 | PARTITIONS_CACHE[None] = prepare_global_partitions() 946 | is_initialized = True 947 | 948 | def get_partition(name, image=None): 949 | partitions = PARTITIONS_CACHE.get(image) 950 | if partitions is None: 951 | partitions = get_partitions(image) 952 | PARTITIONS_CACHE[image] = partitions 953 | # In resinOS 1.8 the root partition is named "resin-root" 954 | if name == "resin-rootA": 955 | names = ["resin-rootA", "resin-root"] 956 | elif name == "flash-rootA": 957 | names = ["flash-rootA", "flash-root"] 958 | else: 959 | names = [name] 960 | for name in names: 961 | part = partitions.get(name) 962 | if part is not None: 963 | return part 964 | 965 | 966 | methods = { 967 | "get_image_info": get_image_info, 968 | "preload": main_preload, 969 | } 970 | 971 | 972 | if __name__ == "__main__": 973 | update_ca_certificates(**SH_OPTS) 974 | for line in sys.stdin: 975 | try: 976 | data = json.loads(line) 977 | method = methods[data["command"]] 978 | result = method(**data.get("parameters", {})) 979 | response = { 980 | "result": result, 981 | "statusCode": 0, 982 | } 983 | except BaseException: 984 | response = { 985 | "error": traceback.format_exc(), 986 | "statusCode": 1, 987 | } 988 | print(json.dumps(response)) 989 | sys.stdout.flush() 990 | if response["statusCode"]: 991 | break 992 | -------------------------------------------------------------------------------- /lib/preload.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as EventEmitter from 'events'; 3 | import * as dockerProgress from 'docker-progress'; 4 | import * as Docker from 'dockerode'; 5 | import * as path from 'path'; 6 | import * as streamModule from 'stream'; 7 | import * as tarfs from 'tar-fs'; 8 | import { promises as fs, constants } from 'fs'; 9 | import * as getPort from 'get-port'; 10 | import * as os from 'os'; 11 | import * as compareVersions from 'compare-versions'; 12 | import type { 13 | Application, 14 | BalenaSDK, 15 | DeviceType, 16 | Pine, 17 | PineDeferred, 18 | Release, 19 | } from 'balena-sdk'; 20 | 21 | const { R_OK, W_OK } = constants; 22 | 23 | const DOCKER_TMPDIR = '/docker_tmpdir'; 24 | const DOCKER_IMAGE_TAG = 'balena/balena-preload'; 25 | const DISK_IMAGE_PATH_IN_DOCKER = '/img/balena.img'; 26 | const SPLASH_IMAGE_PATH_IN_DOCKER = '/img/balena-logo.png'; 27 | const DOCKER_STEP_RE = /Step (\d+)\/(\d+)/; 28 | const CONCURRENT_REQUESTS_TO_REGISTRY = 10; 29 | 30 | const limitedMap = ( 31 | arr: T[], 32 | fn: (currentValue: T, index: number, array: T[]) => Promise, 33 | { 34 | concurrency = CONCURRENT_REQUESTS_TO_REGISTRY, 35 | }: { 36 | concurrency?: number; 37 | } = {}, 38 | ): Promise => { 39 | if (concurrency >= arr.length) { 40 | return Promise.all(arr.map(fn)); 41 | } 42 | return new Promise((resolve, reject) => { 43 | const result: U[] = new Array(arr.length); 44 | let inFlight = 0; 45 | let idx = 0; 46 | const runNext = async () => { 47 | // Store the idx to use for this call before incrementing the main counter 48 | const i = idx; 49 | idx++; 50 | if (i >= arr.length) { 51 | return; 52 | } 53 | try { 54 | inFlight++; 55 | result[i] = await fn(arr[i], i, arr); 56 | void runNext(); 57 | } catch (err) { 58 | // Stop any further iterations 59 | idx = arr.length; 60 | // Clear the results so far for gc 61 | result.length = 0; 62 | reject(err); 63 | } finally { 64 | inFlight--; 65 | if (inFlight === 0) { 66 | resolve(result); 67 | } 68 | } 69 | }; 70 | while (inFlight < concurrency) { 71 | void runNext(); 72 | } 73 | }); 74 | }; 75 | 76 | const GRAPHDRIVER_ERROR = 77 | 'Error starting daemon: error initializing graphdriver: driver not supported'; 78 | const OVERLAY_MODULE_MESSAGE = 79 | 'You need to load the "overlay" module to be able to preload this image: run "sudo modprobe overlay".'; 80 | const DOCKERD_USES_OVERLAY = '--storage-driver=overlay2'; 81 | 82 | const SUPERVISOR_USER_AGENT = 83 | 'Supervisor/v6.6.0 (Linux; Resin OS 2.12.3; prod)'; 84 | 85 | const MISSING_APP_INFO_ERROR_MSG = 86 | 'Could not fetch the target state because of missing application info'; 87 | 88 | class BufferBackedWritableStream extends streamModule.Writable { 89 | chunks: Buffer[] = []; 90 | 91 | _write(chunk, _enc, next) { 92 | this.chunks.push(chunk); 93 | next(); 94 | } 95 | 96 | getData() { 97 | return Buffer.concat(this.chunks); 98 | } 99 | } 100 | 101 | function setBindMount( 102 | hostConfig: Docker.HostConfig, 103 | mounts: Array<[string, string]>, 104 | dockerApiVersion: string, 105 | ) { 106 | if (compareVersions(dockerApiVersion, '1.25') >= 0) { 107 | hostConfig.Mounts = mounts.map(([source, target]) => ({ 108 | Source: path.resolve(source), 109 | Target: target, 110 | Type: 'bind', 111 | Consistency: 'delegated', 112 | })); 113 | } else { 114 | hostConfig.Binds = mounts.map( 115 | ([source, target]) => `${path.resolve(source)}:${target}`, 116 | ); 117 | } 118 | } 119 | 120 | type Layer = { 121 | digest: any; 122 | size: number; 123 | }; 124 | 125 | type Manifest = { 126 | manifest: { 127 | layers: Layer[]; 128 | }; 129 | imageLocation: string; 130 | }; 131 | 132 | type Image = { 133 | is_stored_at__image_location: string; 134 | image_size: string; 135 | }; 136 | 137 | interface ImageInfo { 138 | preloaded_builds: string[]; 139 | supervisor_version: string; 140 | free_space: number; 141 | config: { 142 | deviceType: string; 143 | }; 144 | balena_os_version: string; 145 | } 146 | 147 | /** @const {String} Container name */ 148 | export const CONTAINER_NAME = 'balena-image-preloader'; 149 | 150 | export const applicationExpandOptions = { 151 | owns__release: { 152 | $select: ['id', 'commit', 'end_timestamp', 'composition'], 153 | $expand: { 154 | release_image: { 155 | $select: ['id'], 156 | $expand: { 157 | image: { 158 | $select: ['image_size', 'is_stored_at__image_location'], 159 | }, 160 | }, 161 | }, 162 | }, 163 | $filter: { 164 | status: 'success', 165 | }, 166 | $orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }], 167 | }, 168 | } as const; 169 | 170 | const createContainer = async ( 171 | docker: Docker, 172 | image: string, 173 | splashImage: string | undefined, 174 | dockerPort: number, 175 | proxy: string, 176 | ) => { 177 | const mounts: Array<[string, string]> = []; 178 | const version = await docker.version(); 179 | if (os.platform() === 'linux') { 180 | // In some situations, devices created by `losetup -f` in the container only appear on the host and not in the container. 181 | // See https://github.com/balena-io/balena-cli/issues/1008 182 | mounts.push(['/dev', '/dev']); 183 | } 184 | if (splashImage) { 185 | mounts.push([splashImage, SPLASH_IMAGE_PATH_IN_DOCKER]); 186 | } 187 | 188 | const env = [ 189 | `HTTP_PROXY=${proxy || ''}`, 190 | `HTTPS_PROXY=${proxy || ''}`, 191 | `DOCKER_PORT=${dockerPort || ''}`, 192 | `DOCKER_TMPDIR=${DOCKER_TMPDIR}`, 193 | ]; 194 | 195 | mounts.push([image, DISK_IMAGE_PATH_IN_DOCKER]); 196 | 197 | const containerOptions: Docker.ContainerCreateOptions = { 198 | Image: DOCKER_IMAGE_TAG, 199 | name: CONTAINER_NAME, 200 | AttachStdout: true, 201 | AttachStderr: true, 202 | OpenStdin: true, 203 | Env: env, 204 | Volumes: { 205 | [DOCKER_TMPDIR]: {}, 206 | }, 207 | HostConfig: { 208 | Privileged: true, 209 | }, 210 | }; 211 | // Before api 1.25 bind mounts were going to into HostConfig.Binds 212 | if (containerOptions.HostConfig !== undefined) { 213 | setBindMount(containerOptions.HostConfig, mounts, version.ApiVersion); 214 | if (os.platform() === 'linux') { 215 | containerOptions.HostConfig.NetworkMode = 'host'; 216 | } else { 217 | containerOptions.HostConfig.NetworkMode = 'bridge'; 218 | containerOptions.ExposedPorts = {}; 219 | containerOptions.ExposedPorts[`${dockerPort}/tcp`] = {}; 220 | containerOptions.HostConfig.PortBindings = {}; 221 | containerOptions.HostConfig.PortBindings[`${dockerPort}/tcp`] = [ 222 | { 223 | HostPort: `${dockerPort}`, 224 | HostIp: '', 225 | }, 226 | ]; 227 | } 228 | } 229 | return await docker.createContainer(containerOptions); 230 | }; 231 | 232 | const isReadWriteAccessibleFile = async (image) => { 233 | try { 234 | const [, stats] = await Promise.all([ 235 | // eslint-disable-next-line no-bitwise 236 | fs.access(image, R_OK | W_OK), 237 | fs.stat(image), 238 | ]); 239 | return stats.isFile(); 240 | } catch { 241 | return false; 242 | } 243 | }; 244 | 245 | const deviceTypeQuery = { 246 | $select: 'slug', 247 | $expand: { 248 | is_of__cpu_architecture: { 249 | $select: 'slug', 250 | }, 251 | }, 252 | } as const; 253 | 254 | const getApplicationQuery = (releaseFilter: Pine.Filter) => { 255 | return { 256 | $expand: { 257 | should_be_running__release: { 258 | $select: 'commit', 259 | }, 260 | is_for__device_type: { 261 | $select: 'slug', 262 | $expand: { 263 | is_of__cpu_architecture: { 264 | $select: 'slug', 265 | }, 266 | }, 267 | }, 268 | owns__release: { 269 | $select: ['id', 'commit', 'end_timestamp', 'composition'], 270 | $expand: { 271 | release_image: { 272 | $select: ['id'], 273 | $expand: { 274 | image: { 275 | $select: ['image_size', 'is_stored_at__image_location'], 276 | }, 277 | }, 278 | }, 279 | }, 280 | $filter: releaseFilter, 281 | $orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }], 282 | }, 283 | }, 284 | } as const; 285 | }; 286 | 287 | export class Preloader extends EventEmitter { 288 | application; 289 | stdin; 290 | stdout = new streamModule.PassThrough(); 291 | stderr = new streamModule.PassThrough(); 292 | bufferedStderr = new BufferBackedWritableStream(); 293 | dockerPort; 294 | container: Awaited> | undefined; 295 | state; // device state from the api 296 | freeSpace: number | undefined; // space available on the image data partition (in bytes) 297 | preloadedBuilds: string[] | undefined; // list of preloaded Docker images in the disk image 298 | supervisorVersion: string | undefined; // disk image supervisor version 299 | balenaOSVersion: string | undefined; // OS version from the image's "/etc/issue" file 300 | config: ImageInfo['config'] | undefined; // config.json data from the disk image 301 | deviceTypes: 302 | | Pine.OptionsToResponse< 303 | DeviceType['Read'], 304 | typeof deviceTypeQuery, 305 | undefined 306 | > 307 | | undefined; 308 | balena: BalenaSDK; 309 | 310 | constructor( 311 | balena: BalenaSDK | undefined, 312 | public docker: Docker, 313 | public appId: number | string | undefined, 314 | public commit: string | undefined, 315 | public image: string, 316 | public splashImage: string | undefined, 317 | public proxy: any, 318 | public dontCheckArch: boolean, 319 | public pinDevice = false, 320 | public certificates: string[] = [], 321 | public additionalSpace: number | null = null, 322 | ) { 323 | super(); 324 | this.balena = 325 | balena ?? 326 | (require('balena-sdk') as typeof import('balena-sdk')) // eslint-disable-line @typescript-eslint/no-var-requires 327 | .fromSharedOptions(); 328 | this.stderr.pipe(this.bufferedStderr); // TODO: split stderr and build output ? 329 | } 330 | 331 | /** 332 | * Build the preloader docker image 333 | * @returns Promise 334 | */ 335 | async _build() { 336 | const files = ['Dockerfile', 'requirements.txt', 'src/preload.py']; 337 | const name = 'Building Docker preloader image.'; 338 | this._progress(name, 0); 339 | 340 | const tarStream = tarfs.pack(path.resolve(__dirname, '..'), { 341 | entries: files, 342 | }); 343 | const build = await this.docker.buildImage(tarStream, { 344 | t: DOCKER_IMAGE_TAG, 345 | }); 346 | await new Promise((resolve, reject) => { 347 | this.docker.modem.followProgress( 348 | build, 349 | (error, output) => { 350 | // onFinished 351 | if (!error && output && output.length) { 352 | error = output.pop().error; 353 | } 354 | if (error) { 355 | reject(error); 356 | } else { 357 | this._progress(name, 100); 358 | resolve(); 359 | } 360 | }, 361 | (event) => { 362 | // onProgress 363 | if (event.stream) { 364 | const matches = event.stream.match(DOCKER_STEP_RE); 365 | if (matches) { 366 | this._progress( 367 | name, 368 | (parseInt(matches[1], 10) / (parseInt(matches[2], 10) + 1)) * 369 | 100, 370 | ); 371 | } 372 | this.stderr.write(event.stream); 373 | } 374 | }, 375 | ); 376 | }); 377 | } 378 | 379 | async _fetchDeviceTypes() { 380 | this.deviceTypes = 381 | await this.balena.models.deviceType.getAll(deviceTypeQuery); 382 | } 383 | 384 | async _runWithSpinner(name: string, fn: () => T | Promise): Promise { 385 | this._startSpinner(name); 386 | try { 387 | return await fn(); 388 | } finally { 389 | this._stopSpinner(name); 390 | } 391 | } 392 | 393 | _prepareErrorHandler() { 394 | // Emit an error event if the python script exits with an error 395 | this.container 396 | ?.wait() 397 | .then((data) => { 398 | if (data.StatusCode !== 0) { 399 | const output = this.bufferedStderr.getData().toString('utf8').trim(); 400 | let error; 401 | if ( 402 | output.indexOf(GRAPHDRIVER_ERROR) !== -1 && 403 | output.indexOf(DOCKERD_USES_OVERLAY) !== -1 404 | ) { 405 | error = new this.balena.errors.BalenaError(OVERLAY_MODULE_MESSAGE); 406 | } else { 407 | error = new Error(output); 408 | error.code = data.StatusCode; 409 | } 410 | this.emit('error', error); 411 | } 412 | }) 413 | .catch((error) => this.emit('error', error)); 414 | } 415 | 416 | /** 417 | * Send a command to `preload.py` (running in a Docker container) through 418 | * its stdin, and read a response from its stdout. The stdout response 419 | * must be a JSON object containing a `statusCode` field (integer) and 420 | * optionally `result` and `error` fields. If `statusCode` is the number 421 | * zero, the command execution is considered successful, otherwise it is 422 | * assumed to have failed and the returned promise is rejected with an 423 | * error message including the message provided in the `error` field. 424 | */ 425 | _runCommand(command: string, parameters: { [name: string]: any }) { 426 | return new Promise((resolve, reject) => { 427 | const cmd = JSON.stringify({ command, parameters }) + '\n'; 428 | this.stdout.once('error', reject); 429 | this.stdout.once('data', (data) => { 430 | let strData = data; 431 | try { 432 | strData = data.toString(); 433 | } catch (_e) { 434 | // ignore 435 | } 436 | let response: any = {}; 437 | try { 438 | response = JSON.parse(strData); 439 | } catch (error) { 440 | response.statusCode = 1; 441 | response.error = error; 442 | } 443 | if (response.statusCode === 0) { 444 | resolve(response.result); 445 | } else { 446 | const msg = [ 447 | `An error has occurred executing internal preload command '${command}':`, 448 | cmd, 449 | ]; 450 | if (response.error) { 451 | msg.push( 452 | `Status code: ${response.statusCode}`, 453 | `Error: ${response.error}`, 454 | ); 455 | } else { 456 | msg.push(`Response: ${strData}`); 457 | } 458 | msg.push(''); 459 | reject(new Error(msg.join('\n'))); 460 | } 461 | }); 462 | this.stdin.write(cmd); 463 | }); 464 | } 465 | 466 | _startSpinner(name) { 467 | this.emit('spinner', { name, action: 'start' }); 468 | } 469 | 470 | _stopSpinner(name) { 471 | this.emit('spinner', { name, action: 'stop' }); 472 | } 473 | 474 | _progress(name, percentage) { 475 | this.emit('progress', { name, percentage }); 476 | } 477 | 478 | // Return the version of the target state endpoint used by the supervisor 479 | _getStateVersion() { 480 | if (this._supervisorLT7()) { 481 | return 1; 482 | } else if (this._supervisorLT13()) { 483 | return 2; 484 | } else { 485 | return 3; 486 | } 487 | } 488 | 489 | async _getStateWithRegistration(stateVersion: number) { 490 | if (!this.appId) { 491 | throw new Error(MISSING_APP_INFO_ERROR_MSG); 492 | } 493 | 494 | const uuid = this.balena.models.device.generateUniqueKey(); 495 | 496 | const deviceInfo = await this.balena.models.device.register( 497 | this.appId, 498 | uuid, 499 | ); 500 | await this.balena.pine.patch({ 501 | resource: 'device', 502 | id: deviceInfo.id, 503 | body: { 504 | is_pinned_on__release: this._getRelease().id, 505 | }, 506 | }); 507 | 508 | const { body: state } = await this.balena.request.send({ 509 | headers: { 'User-Agent': SUPERVISOR_USER_AGENT }, 510 | // @ts-expect-error reason unknown 511 | baseUrl: this.balena.pine.API_URL, 512 | url: `device/v${stateVersion}/${uuid}/state`, 513 | }); 514 | this.state = state; 515 | await this.balena.models.device.remove(uuid); 516 | } 517 | 518 | async _getStateFromTargetEndpoint(stateVersion: number) { 519 | if (!this.appId) { 520 | throw new Error(MISSING_APP_INFO_ERROR_MSG); 521 | } 522 | 523 | const release = this._getRelease(); 524 | const [{ uuid: appUuid }, state] = await Promise.all([ 525 | this.balena.models.application.get(this.appId, { 526 | $select: 'uuid', 527 | }), 528 | this.balena.models.device.getSupervisorTargetStateForApp( 529 | this.appId, 530 | release.commit, 531 | ), 532 | ]); 533 | 534 | if (stateVersion === 3) { 535 | // State is keyed by application uuid in target state v3. 536 | // use .local to avoid having to reference by uuid elsewhere on this 537 | // module 538 | state.local = state[appUuid]; 539 | delete state[appUuid]; 540 | } 541 | 542 | this.state = state; 543 | } 544 | 545 | async _getState() { 546 | const stateVersion = this._getStateVersion(); 547 | 548 | if (stateVersion < 3) { 549 | await this._getStateWithRegistration(stateVersion); 550 | } else { 551 | await this._getStateFromTargetEndpoint(stateVersion); 552 | } 553 | } 554 | 555 | async _getImageInfo() { 556 | // returns Promise (device_type, preloaded_builds, free_space and config) 557 | await this._runWithSpinner('Reading image information', async () => { 558 | const info = (await this._runCommand('get_image_info', {})) as ImageInfo; 559 | this.freeSpace = info.free_space; 560 | this.preloadedBuilds = info.preloaded_builds; 561 | this.supervisorVersion = info.supervisor_version; 562 | this.balenaOSVersion = info.balena_os_version; 563 | this.config = info.config; 564 | }); 565 | } 566 | 567 | _getCommit() { 568 | return this.commit || this.application.should_be_running__release[0].commit; 569 | } 570 | 571 | _getRelease() { 572 | const commit = this._getCommit(); 573 | const releases = this.application.owns__release; 574 | if (commit === null && releases.length) { 575 | return releases[0]; 576 | } 577 | const release = _.find(releases, (r) => { 578 | return r.commit.startsWith(commit); 579 | }); 580 | if (!release) { 581 | throw new this.balena.errors.BalenaReleaseNotFound(commit); 582 | } 583 | return release; 584 | } 585 | 586 | _getServicesFromApps(apps) { 587 | const stateVersion = this._getStateVersion(); 588 | // Use the version of the target state endpoint to know 589 | // how to read the apps object 590 | switch (stateVersion) { 591 | case 1: { 592 | // Pre-multicontainer: there is only one image: use the only image from the state endpoint. 593 | const [appV1] = _.values(apps); 594 | return [{ image: appV1.image }]; 595 | } 596 | case 2: { 597 | // Multicontainer: we need to match is_stored_at__image_location with service.image from the state v2 endpoint. 598 | const [appV2] = _.values(apps); 599 | return appV2.services; 600 | } 601 | case 3: { 602 | // v3 target state has a releases property which contains the services 603 | const [appV3] = _.values(apps).filter((a) => a.id === this.appId); 604 | const [release] = _.values(appV3?.releases ?? {}); 605 | return release?.services ?? {}; 606 | } 607 | } 608 | } 609 | 610 | _getImages(): Image[] { 611 | // This method lists the images that need to be preloaded. 612 | // The is_stored_at__image_location attribute must match the image attribute of the app or app service in the state endpoint. 613 | // List images from the release. 614 | const images = this._getRelease().release_image.map((ri) => { 615 | return _.merge({}, ri.image[0], { 616 | is_stored_at__image_location: 617 | ri.image[0].is_stored_at__image_location.toLowerCase(), 618 | }); 619 | }); 620 | 621 | // Multicontainer: we need to match is_stored_at__image_location with service.image from the state v2 endpoint. 622 | const servicesImages = _.map( 623 | this._getServicesFromApps(this.state.local.apps), 624 | (service) => { 625 | return service.image.toLowerCase(); 626 | }, 627 | ); 628 | _.each(images, (image) => { 629 | image.is_stored_at__image_location = _.find( 630 | servicesImages, 631 | (serviceImage) => { 632 | return serviceImage.startsWith(image.is_stored_at__image_location); 633 | }, 634 | ); 635 | }); 636 | 637 | return images; 638 | } 639 | 640 | _getImagesToPreload() { 641 | const preloaded = new Set(this.preloadedBuilds); 642 | const toPreload = new Set(this._getImages()); 643 | for (const image of toPreload) { 644 | if (preloaded.has(image.is_stored_at__image_location.split('@')[0])) { 645 | toPreload.delete(image); 646 | } 647 | } 648 | return Array.from(toPreload); 649 | } 650 | 651 | async registryRequest( 652 | url: 653 | | { 654 | registryUrl: string; 655 | layerUrl: string; 656 | } 657 | | string, 658 | registryToken: string | null, 659 | headers: Record, 660 | responseFormat: RF, 661 | followRedirect: boolean, 662 | ): ReturnType< 663 | typeof this.balena.request.send< 664 | RF extends 'blob' 665 | ? Blob 666 | : RF extends 'json' 667 | ? Record 668 | : never 669 | > 670 | > { 671 | if (typeof url === 'object') { 672 | url = `https://${url.registryUrl}${url.layerUrl}`; 673 | } 674 | return await this.balena.request.send({ 675 | url, 676 | headers: { 677 | ...headers, 678 | ...(registryToken != null && { 679 | Authorization: `Bearer ${registryToken}`, 680 | }), 681 | }, 682 | responseFormat, 683 | followRedirect, 684 | // We don't want to send the token that the SDK has been authenticated with 685 | // the API to the registry 686 | sendToken: false, 687 | refreshToken: false, 688 | }); 689 | } 690 | 691 | async _getLayerSize(registryToken, registryUrl, layerUrl) { 692 | // This gets an approximation of the layer size because: 693 | // * it is the size of the tar file, not the size of the contents of the tar file (the tar file is slightly larger); 694 | // * the gzip footer only gives the size % 32 so it will be incorrect for layers larger than 4GiB 695 | const headers = { 696 | Range: 'bytes=-4', 697 | }; 698 | 699 | let response = await this.registryRequest( 700 | { registryUrl, layerUrl }, 701 | registryToken, 702 | headers, 703 | 'blob', 704 | // We want to avoid re-using the same headers if there is a get redirect, 705 | false, 706 | ); 707 | 708 | if (response.statusCode === 206) { 709 | // no redirect, like in the devenv 710 | } else if ([301, 307].includes(response.statusCode)) { 711 | // redirect, like on production or staging 712 | const redirectUrl = response.headers.get('location'); 713 | if (redirectUrl == null) { 714 | throw new Error( 715 | 'Response status code indicated a redirect but no redirect location was found in the response headers', 716 | ); 717 | } 718 | response = await this.registryRequest( 719 | redirectUrl, 720 | null, 721 | headers, 722 | 'blob', 723 | true, 724 | ); 725 | } else { 726 | throw new Error( 727 | 'Unexpected status code from the registry: ' + response.statusCode, 728 | ); 729 | } 730 | const body = await response.body.arrayBuffer(); 731 | return Buffer.from(body).readUIntLE(0, 4); 732 | } 733 | 734 | _registryUrl(imageLocation) { 735 | // imageLocation: registry2.balena-cloud.com/v2/ad7cd3616b4e72ed51a5ad349e03715e@sha256:4c042f195b59b7d4c492e210ab29ab61694f490a69c65720a5a0121c6277ecdd 736 | const slashIndex = imageLocation.search('/'); 737 | return `${imageLocation.substring(0, slashIndex)}`; 738 | } 739 | 740 | _imageManifestUrl(imageLocation) { 741 | // imageLocation: registry2.balena-cloud.com/v2/ad7cd3616b4e72ed51a5ad349e03715e@sha256:4c042f195b59b7d4c492e210ab29ab61694f490a69c65720a5a0121c6277ecdd 742 | const slashIndex = imageLocation.search('/'); 743 | const atIndex = imageLocation.search('@'); 744 | // 2 times v2: /v2/v2/.... this is expected 745 | return `/v2${imageLocation.substring( 746 | slashIndex, 747 | atIndex, 748 | )}/manifests/${imageLocation.substring(atIndex + 1)}`; 749 | } 750 | 751 | _layerUrl(imageLocation, layerDigest) { 752 | // imageLocation: registry2.balena-cloud.com/v2/ad7cd3616b4e72ed51a5ad349e03715e@sha256:4c042f195b59b7d4c492e210ab29ab61694f490a69c65720a5a0121c6277ecdd 753 | // layerDigest: sha256:cc8d3596dce73cd52b50b9f10a2e4a70eb9db0f7e8ac90e43088b831b72b8ee0 754 | const slashIndex = imageLocation.search('/'); 755 | const atIndex = imageLocation.search('@'); 756 | // 2 times v2: /v2/v2/.... this is expected 757 | return `/v2${imageLocation.substring( 758 | slashIndex, 759 | atIndex, 760 | )}/blobs/${layerDigest}`; 761 | } 762 | 763 | async _getApplicationImagesManifests( 764 | imagesLocations: string[], 765 | registryToken: string, 766 | ) { 767 | return await limitedMap( 768 | imagesLocations, 769 | async (imageLocation) => { 770 | const { body } = await this.registryRequest( 771 | { 772 | registryUrl: this._registryUrl(imageLocation), 773 | layerUrl: this._imageManifestUrl(imageLocation), 774 | }, 775 | registryToken, 776 | {}, 777 | 'json', 778 | true, 779 | ); 780 | return { manifest: body as Manifest['manifest'], imageLocation }; 781 | }, 782 | { concurrency: CONCURRENT_REQUESTS_TO_REGISTRY }, 783 | ); 784 | } 785 | 786 | async _getLayersSizes(manifests: Manifest[], registryToken: string) { 787 | const digests = new Set(); 788 | const layersSizes = new Map(); 789 | const sizeRequests: Array<{ imageLocation: string; layer: Layer }> = []; 790 | for (const manifest of manifests) { 791 | for (const layer of manifest.manifest.layers) { 792 | if (!digests.has(layer.digest)) { 793 | digests.add(layer.digest); 794 | sizeRequests.push({ imageLocation: manifest.imageLocation, layer }); 795 | } 796 | } 797 | } 798 | await limitedMap( 799 | sizeRequests, 800 | async ({ imageLocation, layer }) => { 801 | const size = await this._getLayerSize( 802 | registryToken, 803 | this._registryUrl(imageLocation), 804 | this._layerUrl(imageLocation, layer.digest), 805 | ); 806 | layersSizes.set(layer.digest, { size, compressedSize: layer.size }); 807 | }, 808 | { concurrency: CONCURRENT_REQUESTS_TO_REGISTRY }, 809 | ); 810 | return layersSizes; 811 | } 812 | 813 | async _getApplicationSize() { 814 | const images = this._getImagesToPreload(); 815 | const imagesLocations = _.map(images, 'is_stored_at__image_location'); 816 | const registryToken = await this._getRegistryToken(imagesLocations); 817 | const manifests = await this._getApplicationImagesManifests( 818 | imagesLocations, 819 | registryToken, 820 | ); 821 | const layersSizes = await this._getLayersSizes(manifests, registryToken); 822 | let extra = 0; 823 | for (const { imageLocation, manifest } of manifests) { 824 | const apiSize = _.find(images, { 825 | is_stored_at__image_location: imageLocation, 826 | })?.image_size; 827 | const size = _.sumBy( 828 | manifest.layers, 829 | (layer: Layer) => layersSizes.get(layer.digest).size, 830 | ); 831 | if (apiSize != null && parseInt(apiSize, 10) > size) { 832 | // This means that at least one of the image layers is larger than 4GiB 833 | extra += parseInt(apiSize, 10) - size; 834 | // Extra may be too large if several images share one or more layers larger than 4GiB. 835 | // Maybe we could try to be smarter and mark layers that are smaller than 4GiB as safe (when apiSize <= size). 836 | // If an "unsafe" layer is used in several images, we would add the extra space only once. 837 | } 838 | } 839 | return _.sumBy([...layersSizes.values()], 'size') + extra; 840 | } 841 | 842 | async _getSize() { 843 | const images = this._getImagesToPreload(); 844 | if (images.length === 1) { 845 | // Only one image: we know its size from the api, no need to fetch the layers sizes. 846 | return parseInt(images[0].image_size, 10); 847 | } 848 | // More than one image: sum the sizes of all unique layers of all images to get the application size. 849 | // If we summed the size of each image it would be incorrect as images may share some layers. 850 | return await this._getApplicationSize(); 851 | } 852 | 853 | async _getRequiredAdditionalSpace() { 854 | if (this.additionalSpace !== null) { 855 | return this.additionalSpace; 856 | } 857 | const size = Math.round((await this._getSize()) * 1.4); 858 | return Math.max(0, size - this.freeSpace!); 859 | } 860 | 861 | _supervisorLT7() { 862 | try { 863 | return compareVersions(this.supervisorVersion!, '7.0.0') === -1; 864 | } catch (e) { 865 | // Suppose the supervisor version is >= 7.0.0 when it is not valid semver. 866 | return false; 867 | } 868 | } 869 | 870 | _supervisorLT13() { 871 | try { 872 | return compareVersions(this.supervisorVersion!, '13.0.0') === -1; 873 | } catch (e) { 874 | // This module requires the supervisor image to be tagged. 875 | // The OS stopped tagging supervisor images at some point, and only 876 | // restarted on v2.89.13. This means there is a range of OS versions for which 877 | // supervisorVersion will be `null`. 878 | // If null, assume the version is below 13 879 | return true; 880 | } 881 | } 882 | 883 | async _getRegistryToken(images: string[]) { 884 | const { body } = await this.balena.request.send({ 885 | // @ts-expect-error reason unknwon 886 | baseUrl: this.balena.pine.API_URL, 887 | url: '/auth/v1/token', 888 | qs: { 889 | service: this._registryUrl(images[0]), 890 | scope: images.map( 891 | (imageRepository) => 892 | `repository:${imageRepository.substr( 893 | imageRepository.search('/') + 1, 894 | )}:pull`, 895 | ), 896 | }, 897 | }); 898 | return body.token as string; 899 | } 900 | 901 | async _fetchApplication() { 902 | const { appId } = this; 903 | if (this.application || !appId) { 904 | return; 905 | } 906 | await this._runWithSpinner(`Fetching application ${appId}`, async () => { 907 | const releaseFilter: Pine.Filter = { 908 | status: 'success', 909 | }; 910 | if (this.commit === 'latest') { 911 | const { should_be_running__release } = 912 | await this.balena.models.application.get(appId, { 913 | $select: 'should_be_running__release', 914 | }); 915 | // TODO: Add a check to error if the application is not tracking any release 916 | releaseFilter.id = 917 | (should_be_running__release as PineDeferred | null)!.__id; 918 | } else if (this.commit != null) { 919 | releaseFilter.commit = { $startswith: this.commit }; 920 | } 921 | 922 | const application = await this.balena.models.application.get( 923 | appId, 924 | getApplicationQuery(releaseFilter), 925 | ); 926 | this.setApplication(application); 927 | }); 928 | } 929 | 930 | async _checkImage(image: string) { 931 | const ok = await isReadWriteAccessibleFile(image); 932 | if (!ok) { 933 | console.warn('The image must be a read/write accessible file'); 934 | } 935 | } 936 | 937 | _pluralize(count: number, thing: string) { 938 | return `${count} ${thing}${count !== 1 ? 's' : ''}`; 939 | } 940 | 941 | _deviceTypeArch(slug: string) { 942 | const deviceType = this.deviceTypes?.find((dt) => { 943 | return dt.slug === slug; 944 | }); 945 | if (deviceType === undefined) { 946 | throw new this.balena.errors.BalenaError(`No such deviceType: ${slug}`); 947 | } 948 | return deviceType.is_of__cpu_architecture[0].slug; 949 | } 950 | 951 | async prepare() { 952 | await this._build(); 953 | // Check that the image is a writable file 954 | await this._runWithSpinner( 955 | 'Checking that the image is a writable file', 956 | () => this._checkImage(this.image), 957 | ); 958 | 959 | // Get a free tcp port and balena sdk settings 960 | const port = await this._runWithSpinner('Finding a free tcp port', () => 961 | getPort(), 962 | ); 963 | 964 | this.dockerPort = port; 965 | // Create the docker preloader container 966 | const container = await this._runWithSpinner( 967 | 'Creating preloader container', 968 | () => 969 | createContainer( 970 | this.docker, 971 | this.image, 972 | this.splashImage, 973 | this.dockerPort, 974 | this.proxy, 975 | ), 976 | ); 977 | this.container = container; 978 | await this._runWithSpinner('Starting preloader container', () => 979 | container.start(), 980 | ); 981 | 982 | for (const certificate of this.certificates) { 983 | await this.container.putArchive( 984 | tarfs.pack(path.dirname(certificate), { 985 | entries: [path.basename(certificate)], 986 | }), 987 | { 988 | path: '/usr/local/share/ca-certificates/', 989 | noOverwriteDirNonDir: true, 990 | }, 991 | ); 992 | } 993 | 994 | this._prepareErrorHandler(); 995 | const stream = await this.container.attach({ 996 | stream: true, 997 | stdout: true, 998 | stderr: true, 999 | stdin: true, 1000 | hijack: true, 1001 | }); 1002 | this.stdin = stream; 1003 | this.docker.modem.demuxStream(stream, this.stdout, this.stderr); 1004 | 1005 | await Promise.all([ 1006 | this._getImageInfo(), 1007 | this._fetchDeviceTypes(), 1008 | this._fetchApplication(), 1009 | ]); 1010 | } 1011 | 1012 | async cleanup() { 1013 | // Returns Promise 1014 | // Deletes the container 1015 | await this._runWithSpinner('Cleaning up temporary files', async () => { 1016 | if (this.container) { 1017 | await Promise.all([this.kill(), this.container.wait()]); 1018 | await this.container.remove(); 1019 | } 1020 | }); 1021 | } 1022 | 1023 | async kill() { 1024 | // returns Promise 1025 | if (this.container) { 1026 | return this.container.kill().catch(() => undefined); 1027 | } 1028 | } 1029 | 1030 | _ensureCanPreload() { 1031 | // Throws a BalenaError if preloading is not possible 1032 | let msg: string | undefined; 1033 | 1034 | // No releases 1035 | if (this.application.owns__release.length === 0) { 1036 | msg = 'This application has no successful releases'; 1037 | throw new this.balena.errors.BalenaError(msg); 1038 | } 1039 | 1040 | // Don't preload if the image arch does not match the application arch 1041 | if (this.dontCheckArch === false) { 1042 | const imageArch = this._deviceTypeArch(this.config!.deviceType); 1043 | const applicationArch = 1044 | this.application.is_for__device_type[0].is_of__cpu_architecture[0].slug; 1045 | if ( 1046 | !this.balena.models.os.isArchitectureCompatibleWith( 1047 | imageArch, 1048 | applicationArch, 1049 | ) 1050 | ) { 1051 | msg = `Application architecture (${applicationArch}) and image architecture (${imageArch}) are not compatible.`; 1052 | throw new this.balena.errors.BalenaError(msg); 1053 | } 1054 | } 1055 | 1056 | // Don't preload a multicontainer app on an image which supervisor version is older than 7.0.0 1057 | if (this._getImages().length > 1 && this._supervisorLT7()) { 1058 | msg = `Can't preload a multicontainer app on an image which supervisor version is < 7.0.0 (${this.supervisorVersion}).`; 1059 | throw new this.balena.errors.BalenaError(msg); 1060 | } 1061 | 1062 | // No new images to preload 1063 | if (this._getImagesToPreload().length === 0) { 1064 | msg = 'Nothing new to preload.'; 1065 | throw new this.balena.errors.BalenaError(msg); 1066 | } 1067 | } 1068 | 1069 | _getAppData() { 1070 | if (this._supervisorLT7()) { 1071 | if (this.pinDevice === true) { 1072 | throw new this.balena.errors.BalenaError( 1073 | 'Pinning releases only works with supervisor versions >= 7.0.0', 1074 | ); 1075 | } 1076 | // Add an appId to each app from state v1 (the supervisor needs it) 1077 | // rename environment -> env 1078 | // rename image -> imageId 1079 | // remove serviceId 1080 | return _.map(this.state.local.apps, (value, appId) => { 1081 | return _.merge( 1082 | {}, 1083 | _.omit(value, ['environment', 'image', 'serviceId']), 1084 | { appId, env: value.environment, imageId: value.image }, 1085 | ); 1086 | }); 1087 | } else { 1088 | return _.merge(_.omit(this.state.local, 'name'), { 1089 | pinDevice: this.pinDevice, 1090 | }); 1091 | } 1092 | } 1093 | 1094 | /** 1095 | * Return the splash image path depending on the balenaOS version. 1096 | * (It was renamed from `resin-logo` to `balena-logo` in balenaOS v2.53.0.) 1097 | */ 1098 | _getSplashImagePath() { 1099 | try { 1100 | if (compareVersions(this.balenaOSVersion!, '2.53.0') >= 0) { 1101 | return '/splash/balena-logo.png'; 1102 | } 1103 | } catch (err) { 1104 | // invalid semver (including the empty string, undefined or null) 1105 | } 1106 | return '/splash/resin-logo.png'; 1107 | } 1108 | 1109 | async preload() { 1110 | await this._getState(); 1111 | 1112 | this._ensureCanPreload(); 1113 | const additionalBytes = await this._runWithSpinner( 1114 | 'Estimating required additional space', 1115 | () => this._getRequiredAdditionalSpace(), 1116 | ); 1117 | const images = _.map( 1118 | this._getImagesToPreload(), 1119 | 'is_stored_at__image_location', 1120 | ); 1121 | // Wait for dockerd to start 1122 | await this._runWithSpinner( 1123 | 'Resizing partitions and waiting for dockerd to start', 1124 | () => 1125 | this._runCommand('preload', { 1126 | app_data: this._getAppData(), 1127 | additional_bytes: additionalBytes, 1128 | splash_image_path: this._getSplashImagePath(), 1129 | }), 1130 | ); 1131 | const registryToken = await this._getRegistryToken(images); 1132 | 1133 | const opts = { authconfig: { registrytoken: registryToken } }; 1134 | // Docker connection 1135 | // We use localhost on windows because of this bug in node < 8.10.0: 1136 | // https://github.com/nodejs/node/issues/14900 1137 | const innerDocker = new Docker({ 1138 | host: os.platform() === 'win32' ? 'localhost' : '0.0.0.0', 1139 | port: this.dockerPort, 1140 | }); 1141 | const innerDockerProgress = new dockerProgress.DockerProgress({ 1142 | docker: innerDocker, 1143 | }); 1144 | const pullingProgressName = `Pulling ${this._pluralize( 1145 | images.length, 1146 | 'image', 1147 | )}`; 1148 | // Emit progress events while pulling 1149 | const onProgressHandlers = innerDockerProgress.aggregateProgress( 1150 | images.length, 1151 | (e) => { 1152 | this._progress(pullingProgressName, e.percentage); 1153 | }, 1154 | ); 1155 | await Promise.all( 1156 | images.map(async (image, index) => { 1157 | await innerDockerProgress.pull(image, onProgressHandlers[index], opts); 1158 | }), 1159 | ); 1160 | 1161 | // Signal that we're done to the Python script. 1162 | this.stdin.write('\n'); 1163 | // Wait for the script to unmount the data partition 1164 | await new Promise((resolve, reject) => { 1165 | this.stdout.once('error', reject); 1166 | this.stdout.once('data', resolve); 1167 | }); 1168 | } 1169 | 1170 | setApplication( 1171 | application: NonNullable< 1172 | Pine.OptionsToResponse< 1173 | Application['Read'], 1174 | ReturnType, 1175 | number 1176 | > 1177 | >, 1178 | ) { 1179 | this.appId = application.id; 1180 | this.application = application; 1181 | } 1182 | 1183 | /** 1184 | * @param {string | number} appIdOrSlug 1185 | * @param {string} commit 1186 | * @returns {Promise} 1187 | */ 1188 | async setAppIdAndCommit(appIdOrSlug: string | number, commit: string) { 1189 | this.appId = appIdOrSlug; 1190 | this.commit = commit; 1191 | this.application = null; 1192 | await this._fetchApplication(); 1193 | } 1194 | } 1195 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file 4 | automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY! 5 | This project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## 18.0.5 - 2025-08-07 8 | 9 | 10 |
11 | Bump balena-sdk to v22.0.0 [Otavio Jacobi] 12 | 13 | > ### balena-sdk-22.0.0 - 2025-08-06 14 | > 15 | > * Change device_history methods with separate options [Otavio Jacobi] 16 | > * Make function calls strongly typed with mergePineOptions [Otavio Jacobi] 17 | > * Bump pinejs-client-js to v8.3.0 [Otavio Jacobi] 18 | > * Drop WebResourceFile helper [Otavio Jacobi] 19 | > 20 | > ### balena-sdk-21.7.4 - 2025-07-25 21 | > 22 | > * tests: Reduce the size of the api key names used in tests [Thodoris Greasidis] 23 | > 24 | > ### balena-sdk-21.7.3 - 2025-07-05 25 | > 26 | > * Narrow down the fullUuid type assertion [Otavio Jacobi] 27 | > 28 | > ### balena-sdk-21.7.2 - 2025-07-04 29 | > 30 | > * Migrate all `$orderby` from string form to object form [Otavio Jacobi] 31 | > 32 | > ### balena-sdk-21.7.1 - Invalid date 33 | > 34 | > * application-membership: Replace nested $filter with explicit $any on create [Otavio Jacobi] 35 | > 36 | > ### balena-sdk-21.7.0 - 2025-06-19 37 | > 38 | > * ApplicationMembershipRoles: Add `safe_developer` and `safe_operator` roles [Matthew Yarmolinsky] 39 | > 40 | > ### balena-sdk-21.6.1 - 2025-06-16 41 | > 42 | > * Run CI on supported node version (20) [Otavio Jacobi] 43 | > 44 | > ### balena-sdk-21.6.0 - 2025-06-05 45 | > 46 | > * Replace BillingAccountInfo with extended AccountInfo [Andrea Rosci] 47 | > 48 | > ### balena-sdk-21.5.0 - 2025-06-05 49 | > 50 | > * tests: Skip the org logo tests temporarily [Thodoris Greasidis] 51 | > * Log tests: Increase timeout between logs in setup [joshbwlng] 52 | > * tests: Only retry ratelimited requests with retry delay < 1m [Thodoris Greasidis] 53 | > * Extend the retryRateLimitedRequests sdk option to also accept a function [Thodoris Greasidis] 54 | > 55 | > ### balena-sdk-21.4.7 - 2025-05-29 56 | > 57 | > * Update dependency lint-staged to v16 [balena-renovate[bot]] 58 | > 59 | > ### balena-sdk-21.4.6 - 2025-05-28 60 | > 61 | > * tests/os.download(): Add test for the emitted stream progress events [Thodoris Greasidis] 62 | > 63 | > ### balena-sdk-21.4.5 - 2025-05-28 64 | > 65 | > * Fix log tests race condition [Thodoris Greasidis] 66 | > * Simplify the log tests [Thodoris Greasidis] 67 | > 68 | > ### balena-sdk-21.4.4 - 2025-05-26 69 | > 70 | > * Update balena-request to use native fetch for non-streaming requests [Thodoris Greasidis] 71 | > 72 | > ### balena-sdk-21.4.3 - 2025-05-22 73 | > 74 | > * tests: Use a realistic length for the image content_hash [Thodoris Greasidis] 75 | > 76 | > ### balena-sdk-21.4.2 - 2025-05-20 77 | > 78 | > * logs.history(): Add examples about using the start parameter [Thodoris Greasidis] 79 | > 80 | > ### balena-sdk-21.4.1 - 2025-05-15 81 | > 82 | > * tests/logs: Re-use the same test device across all test cases [Thodoris Greasidis] 83 | > 84 | > ### balena-sdk-21.4.0 - 2025-05-14 85 | > 86 | > * logs: Add support for a start parameter when reading logs [Thodoris Greasidis] 87 | > 88 | > ### balena-sdk-21.3.5 - 2025-05-06 89 | > 90 | > * os: Improve the checks against empty string release tag values [Thodoris Greasidis] 91 | > 92 | > ### balena-sdk-21.3.4 - 2025-05-05 93 | > 94 | > * os.getMaxSatisfyingVersion: Deprecate the 'default' versionRange value [Thodoris Greasidis] 95 | > * os.getMaxSatisfyingVersion: Fix the docs referencing the old recommended & default behavior [Thodoris Greasidis] 96 | > 97 | > ### balena-sdk-21.3.3 - 2025-05-05 98 | > 99 | > * Remove unnecessary null checks [Thodoris Greasidis] 100 | > 101 | > ### balena-sdk-21.3.2 - 2025-04-15 102 | > 103 | > * Remove chai-as-promised from tests [Thodoris Greasidis] 104 | > 105 | > ### balena-sdk-21.3.1 - 2025-04-10 106 | > 107 | > * Update dependency balena-semver to v3 [balena-renovate[bot]] 108 | > 109 | > ### balena-sdk-21.3.0 - 2025-03-26 110 | > 111 | > * device: add `changed_api_heartbeat_state_on__date` to typings [Otavio Jacobi] 112 | > 113 | > ### balena-sdk-21.2.2 - 2025-03-26 114 | > 115 | > * fix linting [Otavio Jacobi] 116 | > 117 | 118 |
119 | 120 | ## 18.0.4 - 2025-05-21 121 | 122 | * Fix balena-preload pip deps [Otavio Jacobi] 123 | 124 | ## 18.0.3 - 2025-03-19 125 | 126 | * Update dependency sh to v1.14.3 [balena-renovate[bot]] 127 | 128 | ## 18.0.2 - 2025-03-19 129 | 130 | * Update alpine Docker tag to v3.21 [balena-renovate[bot]] 131 | 132 | ## 18.0.1 - 2025-03-04 133 | 134 | * HOTFIX: Fix balena-sdk major bump [myarmolinsky] 135 | 136 | ## 18.0.0 - 2025-03-04 137 | 138 | * Replace use of `contains__image` with `release_image` [myarmolinsky] 139 | * Update minimum required node version to 20.12.0 [myarmolinsky] 140 | * Update `balena-sdk` to v21.2.0 [myarmolinsky] 141 | 142 | ## 17.0.0 - 2024-10-21 143 | 144 | * Improve typings [Thodoris Greasidis] 145 | * Stop returning Bluebird promises & drop it from the dependencies [Thodoris Greasidis] 146 | 147 | ## 16.0.0 - 2024-09-17 148 | 149 | * Update `balena-sdk` to 20.1.3 and API v7 [myarmolinsky] 150 | 151 | ## 15.0.6 - 2024-07-12 152 | 153 | * Reuse the registryRequest helper for all registry requests [Thodoris Greasidis] 154 | * Improve the typings for the registry requests [Thodoris Greasidis] 155 | * Use the sdk instead of `request` for registry requests [Thodoris Greasidis] 156 | 157 |
158 | Update balena-sdk from 19.0.1 to 19.7.2 [Thodoris Greasidis] 159 | 160 | > ### balena-sdk-19.7.2 - 2024-07-12 161 | > 162 | > 163 | >
164 | > Update balena-request from 13.3.1 to 13.3.2 [Thodoris Greasidis] 165 | > 166 | >> #### balena-request-13.3.2 - 2024-07-12 167 | >> 168 | >> * Fix always following redirects when followRedirect = false [Thodoris Greasidis] 169 | >> 170 | > 171 | >
172 | > 173 | > 174 | > ### balena-sdk-19.7.1 - 2024-07-08 175 | > 176 | > 177 | >
178 | > Limit pinejs-client-core to ~6.14.0, to fix errors in older TypeScript [Thodoris Greasidis] 179 | > 180 | >> #### pinejs-client-js-6.14.0 - 2023-12-05 181 | >> 182 | >> * Respect the Retry-After header when clients define the getRetryAfterHeader option [Thodoris Greasidis] 183 | >> 184 | >> #### pinejs-client-js-6.13.0 - 2023-07-11 185 | >> 186 | >> * Add support for $duration [Thodoris Greasidis] 187 | >> 188 | >> #### pinejs-client-js-6.12.4 - 2023-05-09 189 | >> 190 | >> * Avoid an unnecessary function creation on each get() call [Thodoris Greasidis] 191 | >> 192 | >> #### pinejs-client-js-6.12.3 - 2022-12-28 193 | >> 194 | >> * CI: Convert tests to TypeScript [Josh Bowling] 195 | >> 196 | >> #### pinejs-client-js-6.12.2 - 2022-11-18 197 | >> 198 | >> * Fix `$orderby: { a: { $count: ... }, $dir: 'asc' }` typings [Thodoris Greasidis] 199 | >> 200 | >> #### pinejs-client-js-6.12.1 - 2022-11-15 201 | >> 202 | >> * Update TypeScript to 4.9.3 [Thodoris Greasidis] 203 | >> 204 | > 205 | >
206 | > 207 | > * Fix the TypeScript incompatibility test [Thodoris Greasidis] 208 | > 209 | > ### balena-sdk-19.7.0 - 2024-07-05 210 | > 211 | > * Add identity provider & saml account model typing [Otavio Jacobi] 212 | > 213 | > ### balena-sdk-19.6.1 - 2024-06-20 214 | > 215 | > * Update TypeScript to 5.5.2 [Thodoris Greasidis] 216 | > 217 | > ### balena-sdk-19.6.0 - 2024-06-20 218 | > 219 | > * Add the application.getAllByOrganization() method [Thodoris Greasidis] 220 | > * Deprecate the application.getAppByOwner() method [Thodoris Greasidis] 221 | > 222 | > ### balena-sdk-19.5.11 - 2024-05-28 223 | > 224 | > * tests: Make the cleanups more precise [Thodoris Greasidis] 225 | > 226 | > ### balena-sdk-19.5.10 - 2024-03-29 227 | > 228 | > * Drop the toWritable helper in favor of TypeScript's satisfies [Thodoris Greasidis] 229 | > 230 | > ### balena-sdk-19.5.9 - 2024-03-29 231 | > 232 | > * os: Update the comments on why we still need to be using the release_tags [Thodoris Greasidis] 233 | > 234 | > ### balena-sdk-19.5.8 - 2024-03-18 235 | > 236 | > * Fix `application.create` method being wrongly marked as deprecated [myarmolinsky] 237 | > 238 | > ### balena-sdk-19.5.7 - 2024-03-08 239 | > 240 | > * Fix missing underscore to describes__device property [Andrea Rosci] 241 | > 242 | > ### balena-sdk-19.5.6 - 2024-03-07 243 | > 244 | > * Update TypeScript to 5.4.2 [Thodoris Greasidis] 245 | > * device-type.getInstructions: Convert etcher link to HTTPS [Vipul Gupta (@vipulgupta2048)] 246 | > 247 | > ### balena-sdk-19.5.5 - 2024-02-26 248 | > 249 | > 250 | >
251 | > Update balena-auth to 6.0.1 [Thodoris Greasidis] 252 | > 253 | >> #### balena-auth-6.0.1 - 2024-02-23 254 | >> 255 | >> * Update jwt-decode to v3 [Thodoris Greasidis] 256 | >> 257 | >> #### balena-auth-6.0.0 - 2024-02-23 258 | >> 259 | >> * Update typescript to 5.3.3 [Thodoris Greasidis] 260 | >> * Move the sources from lib to src [Thodoris Greasidis] 261 | >> * Update @balena/lint to v7 [Thodoris Greasidis] 262 | >> * Stop publishing the lib folder [Thodoris Greasidis] 263 | >> * Drop support for nodejs < 18 [Thodoris Greasidis] 264 | >> * Drop no longer used appveyor.yml [Thodoris Greasidis] 265 | >> 266 | >> #### balena-register-device-9.0.2 - 2024-02-23 267 | >> 268 | >> * Update @balena/lint to v7 [Thodoris Greasidis] 269 | >> * Update balena-request to 13.3.0 [Thodoris Greasidis] 270 | >> 271 | >> #### balena-request-13.3.1 - 2024-02-23 272 | >> 273 | >> * Update balena-auth to 6.0.1 [Thodoris Greasidis] 274 | >> 275 | > 276 | >
277 | > 278 | > 279 | > ### balena-sdk-19.5.4 - 2024-02-14 280 | > 281 | > * Bump balena-request Update balena-request from 13.2.0 to 13.3.0 [Otávio Jacobi] 282 | > 283 | > ### balena-sdk-19.5.3 - 2024-02-14 284 | > 285 | > * Replace deprecated flowzone input tests_run_on [Kyle Harding] 286 | > 287 | > ### balena-sdk-19.5.2 - 2024-02-13 288 | > 289 | > * tests: Reformat describe & it calls to have curly braces [Thodoris Greasidis] 290 | > 291 | > ### balena-sdk-19.5.1 - 2024-02-02 292 | > 293 | > * Update @balena/lint to 7.3.0 [Thodoris Greasidis] 294 | > 295 | > ### balena-sdk-19.5.0 - 2024-01-26 296 | > 297 | > * types: Add the `Organization.is_using__billing_version` property [Thodoris Greasidis] 298 | > 299 | > ### balena-sdk-19.4.0 - 2024-01-23 300 | > 301 | > * Update the deviceType.getInstructions tests [Thodoris Greasidis] 302 | > * os.getSupportedOsUpdateVersions: Add the option to include draft releases [Thodoris Greasidis] 303 | > 304 | >
305 | > Enable OS Updates to pre-release versions of higher base semver [Thodoris Greasidis] 306 | > 307 | >> #### balena-hup-action-utils-6.1.0 - 2024-01-04 308 | >> 309 | >> * Enable OS Updates to pre-release versions of higher base semver [Thodoris Greasidis] 310 | >> 311 | >> #### balena-hup-action-utils-6.0.0 - 2023-12-20 312 | >> 313 | >> * Drop support for TypeScript < 5.3.3 [Thodoris Greasidis] 314 | >> * Drop support for node < v18 [Thodoris Greasidis] 315 | >> * Update dependencies [Thodoris Greasidis] 316 | >> * Move the build step from prepare to prepack [Thodoris Greasidis] 317 | >> 318 | >> #### balena-hup-action-utils-5.0.1 - 2023-07-13 319 | >> 320 | >> * patch: Update flowzone.yml [Kyle Harding] 321 | >> 322 | > 323 | >
324 | > 325 | > * os.getAvailableOsVersions: Add the option to include draft releases [Thodoris Greasidis] 326 | > 327 | > ### balena-sdk-19.3.5 - 2023-12-21 328 | > 329 | > * Update date-fns to v3 [Thodoris Greasidis] 330 | > 331 | > ### balena-sdk-19.3.4 - 2023-12-15 332 | > 333 | > * types/Device: Deprecate the non-existent vpn_address property [Otávio Jacobi] 334 | > 335 | > ### balena-sdk-19.3.3 - 2023-12-15 336 | > 337 | > * types/Device: Deprecate the non-existent state & status_sort_index properties [Thodoris Greasidis] 338 | > 339 | > ### balena-sdk-19.3.2 - 2023-12-08 340 | > 341 | > * test:fast: Run the tests ignoring any linting errors [Thodoris Greasidis] 342 | > * tests: Re-enable the explicit error checks for non-tarball DWB requests [Thodoris Greasidis] 343 | > 344 | > ### balena-sdk-19.3.1 - Invalid date 345 | > 346 | > * Update TypeScript to 5.3.2 [Thodoris Greasidis] 347 | > 348 | > ### balena-sdk-19.3.0 - Invalid date 349 | > 350 | > * tests: Remove the explicit error checks for non-tarball DWB requests [Thodoris Greasidis] 351 | > * tests: Properly cleanup the test orgs [Thodoris Greasidis] 352 | > * tests: Reduce the request batching chunk size to speed up tests [Thodoris Greasidis] 353 | > * Add option for configuring the request batching chunk size [Thodoris Greasidis] 354 | > 355 | > ### balena-sdk-19.2.0 - 2023-11-13 356 | > 357 | > * Add organization logo to organization [Otávio Jacobi] 358 | > 359 | > ### balena-sdk-19.1.0 - 2023-11-06 360 | > 361 | > * Add the retryRateLimitedRequests sdk option for retrying after HTTP 429s [Thodoris Greasidis] 362 | > 363 | 364 |
365 | 366 | ## 15.0.5 - 2024-04-09 367 | 368 | * Remove unused dependencies [Otavio Jacobi] 369 | 370 | ## 15.0.4 - 2024-04-09 371 | 372 | * Remove unused resin-cli-visuals dependency [Otavio Jacobi] 373 | 374 | ## 15.0.3 - 2024-04-09 375 | 376 | * Update resin-cli-visuals to v2 [Thodoris Greasidis] 377 | 378 | ## 15.0.2 - 2024-02-13 379 | 380 | * Update to eslint based linter [Ken Bannister] 381 | 382 | ## 15.0.1 - 2023-10-26 383 | 384 | 385 |
386 | Update balena-sdk to 19.0.1 [Otávio Jacobi] 387 | 388 | > ### balena-sdk-19.0.1 - 2023-10-19 389 | > 390 | > * Fix test workflow to run on node 18 [Otávio Jacobi] 391 | > 392 | > ### balena-sdk-19.0.0 - 2023-10-16 393 | > 394 | > 395 | >
396 | > **BREAKING**: Drop support to node < 18 [Otávio Jacobi] 397 | > 398 | >> #### balena-register-device-9.0.1 - 2023-10-11 399 | >> 400 | >> * Fix balena-request peer dependency [Otávio Jacobi] 401 | >> 402 | >> #### balena-register-device-9.0.0 - 2023-10-11 403 | >> 404 | >> * Drop supoport for node 14 & 16 [Otávio Jacobi] 405 | >> 406 | >> #### balena-request-13.0.0 - 2023-10-11 407 | >> 408 | >> * Drop support for node 14 & 16 [Otávio Jacobi] 409 | >> 410 | > 411 | >
412 | > 413 | > 414 | > ### balena-sdk-18.3.0 - 2023-10-09 415 | > 416 | > * jwt: Deprecate the profile fields in favor of the user_profile [Thodoris Greasidis] 417 | > * jwt: Deprecate the intercom fields [Thodoris Greasidis] 418 | > * jwt: Deprecate the features fields [Thodoris Greasidis] 419 | > * jwt: Deprecate thw loginAs field [Thodoris Greasidis] 420 | > * jwt: Deprecate username & created_at in favor of the user resource [Thodoris Greasidis] 421 | > * Add typings for the user_profile resource [Thodoris Greasidis] 422 | > 423 | > ### balena-sdk-18.2.0 - 2023-09-26 424 | > 425 | > * Update @balena/lint to 7.2.0 [Thodoris Greasidis] 426 | > * Deprecate the social_service_account property of the JWTUser [Thodoris Greasidis] 427 | > * Add typings for the social_service_account resource [Thodoris Greasidis] 428 | > 429 | > ### balena-sdk-18.1.4 - 2023-08-24 430 | > 431 | > * Update TypeScript to 5.2.2 [Thodoris Greasidis] 432 | > 433 | > ### balena-sdk-18.1.3 - 2023-08-23 434 | > 435 | > * tests/os: Refactor some promise tests to async await [Thodoris Greasidis] 436 | > * Fix os.getSupervisorReleaseByDeviceType test to work on balenaMachine [Thodoris Greasidis] 437 | > 438 | >
439 | > Update balena-request from 12.0.2 to 12.0.4 [Thodoris Greasidis] 440 | > 441 | >> #### balena-request-12.0.4 - 2023-08-23 442 | >> 443 | >> * Refactor the interceptors to stop using .reduce() [Thodoris Greasidis] 444 | >> 445 | >> #### balena-request-12.0.3 - 2023-08-09 446 | >> 447 | >> * Avoid deep imports from balena-auth [Thodoris Greasidis] 448 | >> * Update balena-auth to 5.1.0 [Thodoris Greasidis] 449 | >> 450 | > 451 | >
452 | > 453 | > 454 | > ### balena-sdk-18.1.2 - 2023-08-23 455 | > 456 | > * organization-invite: Fix throwing a typed error when passing an unkonwn role [Thodoris Greasidis] 457 | > * application-invite: Fix throwing a typed error when passing an unkonwn role [Thodoris Greasidis] 458 | > * tests: Fix bugs that linting surfaced [Thodoris Greasidis] 459 | > * Update @balena/lint to 7.0.1 [Thodoris Greasidis] 460 | > 461 | > ### balena-sdk-18.1.1 - 2023-08-22 462 | > 463 | > * logs: Emit errors when initializing the SDK with debug: true [Thodoris Greasidis] 464 | > 465 | > ### balena-sdk-18.1.0 - 2023-08-22 466 | > 467 | > * Improve the auth.getActorId() tests [Thodoris Greasidis] 468 | > * auth.getUserInfo: Add the actor id to the returned values [Thodoris Greasidis] 469 | > 470 | > ### balena-sdk-18.0.2 - 2023-08-18 471 | > 472 | > * patch: bump lint-staged from 13.3.0 to 14.0.0 [Thodoris Greasidis] 473 | > 474 | > ### balena-sdk-18.0.1 - 2023-08-18 475 | > 476 | > * Replace dependabot with renovate [Thodoris Greasidis] 477 | > 478 | 479 |
480 | 481 | ## 15.0.0 - 2023-10-26 482 | 483 | * Drop support for Node.js v16 [Otávio Jacobi] 484 | 485 | ## 14.0.3 - 2023-09-25 486 | 487 | * Fix preload with commit hash [Otávio Jacobi] 488 | 489 | ## 14.0.2 - 2023-08-21 490 | 491 | 492 |
493 | Update to balena-sdk 18.0.0 [Otávio Jacobi] 494 | 495 | > ### balena-sdk-18.0.0 - 2023-08-17 496 | > 497 | > * **BREAKING**: Remove the device-type.json state & name normalization [Thodoris Greasidis] 498 | > * **BREAKING**: Drop auth.getUserActorId in favor of auth.getActorId [Otávio Jacobi] 499 | > * auth: Add getActorId [Otávio Jacobi] 500 | > * **BREAKING**: Drop auth.getUserId in favor of auth.getUserInfo [Otávio Jacobi] 501 | > * **BREAKING**: Drop auth.getEmail in favor of auth.getUserInfo [Otávio Jacobi] 502 | > * auth: Add getUserInfo [Otávio Jacobi] 503 | > * **BREAKING**: Drop pre-Resin OS v1 device.os_version normalization [Thodoris Greasidis] 504 | > * **BREAKING**: Support non-user API keys in auth.isLoggedIn() & whoami() [Otávio Jacobi] 505 | > * **BREAKING**: Drop support to node < 16 [Otávio Jacobi] 506 | > 507 | > ### balena-sdk-17.12.1 - 2023-08-09 508 | > 509 | > * Fix pointing browser es2018 settings-client to the es2015 one [Thodoris Greasidis] 510 | > * Point browser bundlers to the appropriate handlebars entrypoint [Thodoris Greasidis] 511 | > 512 | > ### balena-sdk-17.12.0 - 2023-08-09 513 | > 514 | > * tests: Reduce the polyfills used in webpack [Thodoris Greasidis] 515 | > * Avoid loading balena-settings-client in browsers using the browser field [Thodoris Greasidis] 516 | > 517 | > ### balena-sdk-17.11.0 - 2023-08-08 518 | > 519 | > 520 | >
521 | > Add support for creating isolated in-memory instances [Thodoris Greasidis] 522 | > 523 | >> #### balena-auth-5.1.0 - 2023-07-28 524 | >> 525 | >> * Add support for isolated instances by passing dataDirectory: false [Thodoris Greasidis] 526 | >> 527 | >> #### balena-auth-5.0.1 - 2023-07-28 528 | >> 529 | >> * Add multiple instance isolation tests [Thodoris Greasidis] 530 | >> 531 | > 532 | >
533 | > 534 | > 535 | > ### balena-sdk-17.10.2 - 2023-07-25 536 | > 537 | > 538 | >
539 | > Update balena-request to 12.0.2 [Thodoris Greasidis] 540 | > 541 | >> #### balena-request-12.0.2 - 2023-07-25 542 | >> 543 | >> * Make `url` a normal dependency [Thodoris Greasidis] 544 | >> 545 | > 546 | >
547 | > 548 | > 549 | > ### balena-sdk-17.10.1 - 2023-07-25 550 | > 551 | > 552 | >
553 | > Update dependenecies [Thodoris Greasidis] 554 | > 555 | >> #### balena-auth-5.0.0 - 2023-07-24 556 | >> 557 | >> 558 | >>
559 | >> Update balena-settings-storage to 8.0.0 [Thodoris Greasidis] 560 | >> 561 | >>> ##### balena-settings-storage-8.0.0 - 2023-07-24 562 | >>> 563 | >>> * virtual-storage: Use an object without a prototype as the store [Thodoris Greasidis] 564 | >>> * Specify a browser entry point [Thodoris Greasidis] 565 | >>> * Use es6 exports [Thodoris Greasidis] 566 | >>> * Update TypeScript to 5.1.6 [Thodoris Greasidis] 567 | >>> * Drop support for nodejs < 14 [Thodoris Greasidis] 568 | >>> 569 | >>> ##### balena-settings-storage-7.0.2 - 2022-11-08 570 | >>> 571 | >>> * Update balena-errors from v4.7.1 to v4.7.3 [JSReds] 572 | >>> 573 | >>> ##### balena-settings-storage-7.0.1 - 2022-11-01 574 | >>> 575 | >>> * Fix tests on node18 [Thodoris Greasidis] 576 | >>> * Replace balenaCI with flowzone [JSReds] 577 | >>> 578 | >> 579 | >>
580 | >> 581 | >> * Update dependencies [Thodoris Greasidis] 582 | >> * Drop support for nodejs < 14 [Thodoris Greasidis] 583 | >> 584 | >> #### balena-auth-4.2.1 - 2023-07-13 585 | >> 586 | >> * patch: Update flowzone.yml [Kyle Harding] 587 | >> 588 | >> #### balena-auth-4.2.0 - 2023-05-25 589 | >> 590 | >> * Add a get2FAStatus() method [Thodoris Greasidis] 591 | >> 592 | >> #### balena-auth-4.1.3 - 2023-05-25 593 | >> 594 | >> * Fix async tests not waiting for the result [Thodoris Greasidis] 595 | >> 596 | >> #### balena-auth-4.1.2 - 2022-09-26 597 | >> 598 | >> * Delete redundant .resinci.yml [Thodoris Greasidis] 599 | >> 600 | >> #### balena-auth-4.1.1 - 2022-09-22 601 | >> 602 | >> * Replace balenaCI with flowzone [Thodoris Greasidis] 603 | >> 604 | >> #### balena-register-device-8.0.7 - 2023-07-24 605 | >> 606 | >> * Update balena-auth to 5.0.0 & balena-request to 12.0.1 [Thodoris Greasidis] 607 | >> * Use typescript via a devDependency rather than npx [Thodoris Greasidis] 608 | >> 609 | >> #### balena-register-device-8.0.6 - 2023-07-24 610 | >> 611 | >> * Update mockttp to 3.8.0 [Thodoris Greasidis] 612 | >> 613 | >> #### balena-register-device-8.0.5 - 2023-06-01 614 | >> 615 | >> * Update minimum node version to v14 [Kyle Harding] 616 | >> * Update flowzone.yml [Kyle Harding] 617 | >> 618 | >> #### balena-register-device-8.0.4 - 2022-09-26 619 | >> 620 | >> * Delete redundant .resinci.yml [Thodoris Greasidis] 621 | >> 622 | >> #### balena-register-device-8.0.3 - 2022-09-22 623 | >> 624 | >> * Fix overriding the whole webpack resolve section of karma tests [Thodoris Greasidis] 625 | >> 626 | >> #### balena-register-device-8.0.2 - 2022-09-22 627 | >> 628 | >> * Fix key uniqueness check [Thodoris Greasidis] 629 | >> * Convert the tests to TypeScript [Thodoris Greasidis] 630 | >> * Fix karma browser tests in node 18 [Thodoris Greasidis] 631 | >> * Specify the supported node engines in the package.json [Thodoris Greasidis] 632 | >> 633 | >> #### balena-register-device-8.0.1 - 2022-09-21 634 | >> 635 | >> * Replace balenaCI with flowzone [Thodoris Greasidis] 636 | >> 637 | >> #### balena-request-12.0.1 - 2023-07-24 638 | >> 639 | >> * Update balena-auth to 5.0.0 [Thodoris Greasidis] 640 | >> 641 | >> #### balena-request-12.0.0 - 2023-07-14 642 | >> 643 | >> * Update TypeScript to 5.1.6 [Thodoris Greasidis] 644 | >> * Update mockttp to v3.8.0 [Thodoris Greasidis] 645 | >> * Drop support for node < 14 [Thodoris Greasidis] 646 | >> * Add querystring-es3 polyfill to fix browser tests [Thodoris Greasidis] 647 | >> * tsconfig: Enable skipLibCheck to avoid mockttp nested dependency errors [Thodoris Greasidis] 648 | >> * Update TypeScript to 4.9.5 [Thodoris Greasidis] 649 | >> * patch: Update flowzone.yml [Kyle Harding] 650 | >> 651 | >> #### balena-request-11.5.10 - 2022-11-02 652 | >> 653 | >> * Update balena-errors to v4.7.3 [JSReds] 654 | >> 655 | >> #### balena-request-11.5.9 - 2022-09-26 656 | >> 657 | >> * Delete redundant .resinci.yml [Thodoris Greasidis] 658 | >> 659 | >> #### balena-request-11.5.8 - 2022-09-22 660 | >> 661 | >> * Fix overriding the whole webpack resolve section of karma tests [Thodoris Greasidis] 662 | >> 663 | >> #### balena-request-11.5.7 - 2022-09-22 664 | >> 665 | >> * Replace balenaCI with flowzone [Thodoris Greasidis] 666 | >> * Fix tests in node 18 [Thodoris Greasidis] 667 | >> * Specify the supported node engines in the package.json [Thodoris Greasidis] 668 | >> 669 | >> #### balena-request-11.5.6 - 2022-09-22 670 | >> 671 | >> * Fix the typings to properly mark the auth parameter as optional [Thodoris Greasidis] 672 | >> * Update TypeScript to 4.8.3 [Thodoris Greasidis] 673 | >> 674 | >> #### balena-request-11.5.5 - 2022-04-06 675 | >> 676 | >> * Fix extracting the response error from object response bodies [Thodoris Greasidis] 677 | >> 678 | >> #### balena-request-11.5.4 - 2022-04-06 679 | >> 680 | >> * Drop explicit karma-chrome-launcher devDependency [Thodoris Greasidis] 681 | >> 682 | >> #### balena-request-11.5.3 - 2022-04-05 683 | >> 684 | >> * Use response error as response message if there is one [Matthew Yarmolinsky] 685 | >> 686 | >> #### balena-request-11.5.2 - 2022-04-04 687 | >> 688 | >> * Drop circle.yml [Thodoris Greasidis] 689 | >> 690 | >> #### balena-request-11.5.1 - 2022-04-04 691 | >> 692 | >> * Drop mochainon & bump karma [Thodoris Greasidis] 693 | >> 694 | >> #### balena-request-11.5.0 - 2021-11-28 695 | >> 696 | >> * Convert tests to JavaScript and drop coffeescript [Thodoris Greasidis] 697 | >> * Fix the jsdoc generation [Thodoris Greasidis] 698 | >> * Convert to typescript and publish typings [Thodoris Greasidis] 699 | >> 700 | >> #### balena-request-11.4.2 - 2021-09-20 701 | >> 702 | >> * Allow overriding the default zlib flush setting [Kyle Harding] 703 | >> 704 | >> #### balena-request-11.4.1 - 2021-08-27 705 | >> 706 | >> * Allow more lenient gzip decompression [Kyle Harding] 707 | >> 708 | >> #### balena-request-11.4.0 - 2021-03-12 709 | >> 710 | >> * Update fetch-ponyfill to v7 [Thodoris Greasidis] 711 | >> 712 | >> #### balena-request-11.3.0 - 2021-03-12 713 | >> 714 | >> * Switch to the versioned token refresh endpoint [Thodoris Greasidis] 715 | >> 716 | >> #### balena-request-11.2.1 - 2021-03-12 717 | >> 718 | >> * Prevent token refresh when no base url is provided [Thodoris Greasidis] 719 | >> 720 | >> #### balena-request-11.2.0 - 2020-11-12 721 | >> 722 | >> * Update balena-auth from 4.0.0 to 4.1.0 [josecoelho] 723 | >> 724 | >> #### balena-request-11.1.1 - 2020-08-13 725 | >> 726 | >> * Stop refreshing the token on absolute urls [Thodoris Greasidis] 727 | >> 728 | >> #### balena-request-11.1.0 - 2020-07-16 729 | >> 730 | >> * Add lazy loading for most modules [Pagan Gazzard] 731 | >> 732 | >> #### balena-request-11.0.4 - 2020-07-14 733 | >> 734 | >> * Fix body overwriting on nodejs [Pagan Gazzard] 735 | >> 736 | >> #### balena-request-11.0.3 - 2020-07-13 737 | >> 738 | >> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard] 739 | >> 740 | >> #### balena-request-11.0.2 - 2020-07-06 741 | >> 742 | >> * Fix tslib dependency [Pagan Gazzard] 743 | >> 744 | >> #### balena-request-11.0.1 - 2020-07-03 745 | >> 746 | >> * Fix passing baseUrl to refreshToken if the request uses an absolute url [Pagan Gazzard] 747 | >> 748 | > 749 | >
750 | > 751 | > 752 | > ### balena-sdk-17.10.0 - 2023-07-11 753 | > 754 | > * service: Allow passing an application-service_name pair as a parameter [Thodoris Greasidis] 755 | > 756 | > ### balena-sdk-17.9.0 - 2023-07-11 757 | > 758 | > * device.serviceVar: Allow passing a service name as a parameter [Thodoris Greasidis] 759 | > 760 | > ### balena-sdk-17.8.0 - 2023-07-10 761 | > 762 | > * billing: Add `removeBillingInfo` method for removing billing info [myarmolinsky] 763 | > 764 | > ### balena-sdk-17.7.1 - 2023-07-09 765 | > 766 | > * deviceType.getBySlugOrName: Use a clearer var name in the docs example [Thodoris Greasidis] 767 | > 768 | > ### balena-sdk-17.7.0 - 2023-07-06 769 | > 770 | > * Add typings for the organization.is_frozen computed term [Thodoris Greasidis] 771 | > 772 | > ### balena-sdk-17.6.0 - 2023-07-05 773 | > 774 | > * application.create: Enable creating fleets with archived device types [myarmolinsky] 775 | > 776 | > ### balena-sdk-17.5.0 - 2023-06-27 777 | > 778 | > * Add `owns__credit_bundle` typing for `Organization` [myarmolinsky] 779 | > 780 | > ### balena-sdk-17.4.0 - 2023-06-19 781 | > 782 | > * Add typings for Credits Notifications [myarmolinsky] 783 | > 784 | > ### balena-sdk-17.3.2 - 2023-06-19 785 | > 786 | > * util: Simplify the listImagesFromTargetState helper [Thodoris Greasidis] 787 | > 788 | > ### balena-sdk-17.3.1 - 2023-06-17 789 | > 790 | > * Fix prettier complaining on windows runners [Thodoris Greasidis] 791 | > * deviceType.getInstructions: Improve the return type [Thodoris Greasidis] 792 | > * Fix browser tests now failing to find a polyfill for querystring [Thodoris Greasidis] 793 | > 794 | > ### balena-sdk-17.3.0 - 2023-06-05 795 | > 796 | > * os: Export the OsDownloadOptions type [Thodoris Greasidis] 797 | > 798 | > ### balena-sdk-17.2.3 - 2023-06-04 799 | > 800 | > * tsconfig: Allow noImplicitThis in the tests [Thodoris Greasidis] 801 | > * tsconfig: Switch to strict compilation to fix the 5.1 errors [Thodoris Greasidis] 802 | > * Update TypeScript to 5.1.3 [Thodoris Greasidis] 803 | > 804 | > ### balena-sdk-17.2.2 - 2023-06-01 805 | > 806 | > * Access other models internally via the shared current sdk instance [Thodoris Greasidis] 807 | > 808 | > ### balena-sdk-17.2.1 - 2023-06-01 809 | > 810 | > * Convert the remaining .js tests to .ts [Thodoris Greasidis] 811 | > 812 | > ### balena-sdk-17.2.0 - 2023-06-01 813 | > 814 | > * Extends batch device actions to accept arrays of full UUIDs [Thodoris Greasidis] 815 | > * device.startOsUpdate: Add support for providing an array of UUIDs [Thodoris Greasidis] 816 | > 817 | > ### balena-sdk-17.1.4 - Invalid date 818 | > 819 | > * Add 2fa tests [Otávio Jacobi] 820 | > 821 | >
822 | > Fix auth.twoFactor.isEnabled() regression returning always true [Thodoris Greasidis] 823 | > 824 | >> #### balena-auth-4.2.0 - 2023-05-25 825 | >> 826 | >> * Add a get2FAStatus() method [Thodoris Greasidis] 827 | >> 828 | >> #### balena-auth-4.1.3 - 2023-05-25 829 | >> 830 | >> * Fix async tests not waiting for the result [Thodoris Greasidis] 831 | >> 832 | >> #### balena-auth-4.1.2 - 2022-09-26 833 | >> 834 | >> * Delete redundant .resinci.yml [Thodoris Greasidis] 835 | >> 836 | >> #### balena-auth-4.1.1 - 2022-09-22 837 | >> 838 | >> * Replace balenaCI with flowzone [Thodoris Greasidis] 839 | >> 840 | > 841 | >
842 | > 843 | > 844 | > ### balena-sdk-17.1.3 - 2023-05-29 845 | > 846 | > * Add support to short uuid on device.serviceVar.set [Otávio Jacobi] 847 | > 848 | > ### balena-sdk-17.1.2 - 2023-05-25 849 | > 850 | > * Switch to a stricter request limiting queuing mode [Thodoris Greasidis] 851 | > 852 | > ### balena-sdk-17.1.1 - 2023-05-25 853 | > 854 | > * Drop the callback examples from the docs [Thodoris Greasidis] 855 | > 856 | > ### balena-sdk-17.1.0 - 2023-05-24 857 | > 858 | > * Add the requestLimit & requestLimitInterval options to the SDK factory [Thodoris Greasidis] 859 | > 860 | > ### balena-sdk-17.0.2 - 2023-05-24 861 | > 862 | > * Update dependencies [Thodoris Greasidis] 863 | > 864 | > ### balena-sdk-17.0.1 - 2023-05-24 865 | > 866 | > * Add a method to retrieve the supervisor image for a DT [Edwin Joassart] 867 | > * Add util to list images referenced in a target state v3 [Edwin Joassart] 868 | > * Add a method for retrieving an application's virtual device target supervisor state [Edwin Joassart] 869 | > 870 | 871 |
872 | 873 | ## 14.0.1 - 2023-08-02 874 | 875 | * Use SDK getSupervisorTargetStateForApp when getting supervisor state [Otávio Jacobi] 876 | 877 | ## 14.0.0 - 2023-05-23 878 | 879 | * Improve typings & source type safety [Thodoris Greasidis] 880 | * Update dev dependencies [Thodoris Greasidis] 881 | * Update to balena-sdk 17.0.0 [Thodoris Greasidis] 882 | * Emit type declaration files [Thodoris Greasidis] 883 | * Switch to es6 module exports [Thodoris Greasidis] 884 | * Require es2019 capable runtime [Thodoris Greasidis] 885 | * Drop support for Node.js v12 & v14 [Thodoris Greasidis] 886 | 887 | ## 13.0.0 - 2022-12-22 888 | 889 | * major: removes Edison DT code [JOASSART Edwin] 890 | 891 | ## 12.3.0 - 2022-12-22 892 | 893 | * minor: build for every major node version >=12 [Edwin Joassart] 894 | 895 | ## 12.2.0 - 2022-12-12 896 | 897 | * fix stdin with docker 20.10.17 [Edwin Joassart] 898 | 899 | ## 12.1.1 - 2022-11-16 900 | 901 | * move to flowzone set npm engine to 12 remove resin-ci [Edwin Joassart] 902 | 903 | ## 12.1.0 - 2022-05-26 904 | 905 | * Add support for preloading v3 target state format [pipex] 906 | 907 | ## 12.0.1 - 2022-05-10 908 | 909 | * Update supervisor image regex to include tagged images [Kyle Harding] 910 | 911 | ## 12.0.0 - 2022-01-27 912 | 913 | * Improve types [Thodoris Greasidis] 914 | * Stop relying on the /device-types/v1 endpoints [Thodoris Greasidis] 915 | * Bump TypeScript to v4.5 [Thodoris Greasidis] 916 | 917 |
918 | Bump balena-sdk to v16 [Thodoris Greasidis] 919 | 920 | > ### balena-sdk-16.0.0 - 2021-11-28 921 | > 922 | > * **BREAKING**: Merge the hostApp model into the OS model [Thodoris Greasidis] 923 | > * **BREAKING** Drop os.getSupportedVersions() method in favor of hostapp.getAvailableOsVersions() [Thodoris Greasidis] 924 | > * os.getMaxSatisfyingVersion: Add optional param to choose OS line type [Thodoris Greasidis] 925 | > * os.getMaxSatisfyingVersion: Include ESR versions [Thodoris Greasidis] 926 | > * os.getMaxSatisfyingVersion: Switch to use hostApps [Thodoris Greasidis] 927 | > * hostapp.getAvailableOsVersions: Add single device type argument overload [Thodoris Greasidis] 928 | > * hostapp.getAllOsVersions: Add single device type argument overload [Thodoris Greasidis] 929 | > * models.hostapp: Add a getAvailableOsVersions() convenience method [Thodoris Greasidis] 930 | > * Support optional extra PineOptions in hostapp.getAllOsVersions() [Thodoris Greasidis] 931 | > * **BREAKING** Include invalidated versions in hostapp.getAllOsVersions() [Thodoris Greasidis] 932 | > * models/application: Add getDirectlyAccessible & getAllDirectlyAccessible [Thodoris Greasidis] 933 | > * application.get: Add 'directly_accessible' convenience filter param [Thodoris Greasidis] 934 | > * application.getAll: Add 'directly_accessible' convenience filter param [Thodoris Greasidis] 935 | > * **BREAKING** Change application.getAll to include public apps [Thodoris Greasidis] 936 | > * **BREAKING** Drop targeting/retrieving apps by name in favor of slugs [Thodoris Greasidis] 937 | > * Bump minimum supported Typescript to v4.5.2 [Thodoris Greasidis] 938 | > * **BREAKING**: Stop actively supporting node 10 [Thodoris Greasidis] 939 | > * **BREAKING** Drop application.getAllWithDeviceServiceDetails() [Thodoris Greasidis] 940 | > * **BREAKING** Change apiKey.getAll() to return all key variants [Thodoris Greasidis] 941 | > * types: Drop is_in_local_mode from the Device model [Thodoris Greasidis] 942 | > * types: Drop user__is_member_of__application in favor of the term form [Thodoris Greasidis] 943 | > * typings: Drop Subscription's discounts__plan_addon property [Thodoris Greasidis] 944 | > * typings: Stop extending the JWTUser type in the User model [Thodoris Greasidis] 945 | > * models/config: Change the BETA device type state to NEW [Thodoris Greasidis] 946 | > * typings: Drop the PineWithSelectOnGet type [Thodoris Greasidis] 947 | > * Remove my_application from the supported resources [Thodoris Greasidis] 948 | > * typings: Properly type some Device properties [Thodoris Greasidis] 949 | > * typings: Drop the DeviceWithImageInstalls type [Thodoris Greasidis] 950 | > 951 | > ### balena-sdk-15.59.2 - 2021-11-28 952 | > 953 | > 954 | >
955 | > Update balena-request to 11.5.0 [Thodoris Greasidis] 956 | > 957 | >> #### balena-request-11.5.0 - 2021-11-28 958 | >> 959 | >> * Convert tests to JavaScript and drop coffeescript [Thodoris Greasidis] 960 | >> * Fix the jsdoc generation [Thodoris Greasidis] 961 | >> * Convert to typescript and publish typings [Thodoris Greasidis] 962 | >> 963 | >
964 | > 965 | > 966 | > ### balena-sdk-15.59.1 - 2021-11-28 967 | > 968 | > * Fix the typings of the Image contract field [Thodoris Greasidis] 969 | > * Fix the typings for the Release contract field [Thodoris Greasidis] 970 | > 971 | > ### balena-sdk-15.59.0 - 2021-11-24 972 | > 973 | > * Add release setIsInvalidated function [Matthew Yarmolinsky] 974 | > 975 | > ### balena-sdk-15.58.1 - 2021-11-17 976 | > 977 | > * Update typescript to 4.5.2 [Thodoris Greasidis] 978 | > 979 | > ### balena-sdk-15.58.0 - 2021-11-16 980 | > 981 | > * models/release: Add note() method [Thodoris Greasidis] 982 | > * typings: Add the release.invalidation_reason property [Thodoris Greasidis] 983 | > * typings: Add the release.note property [Thodoris Greasidis] 984 | > 985 | > ### balena-sdk-15.57.2 - 2021-11-15 986 | > 987 | > * tests/logs: Increase the wait time for retrieving the subscribed logs [Thodoris Greasidis] 988 | > * tests/logs: Refactor to async-await [Thodoris Greasidis] 989 | > 990 | > ### balena-sdk-15.57.1 - 2021-11-11 991 | > 992 | > * typings: Fix $filters for resources with non numeric ids [Thodoris Greasidis] 993 | > * typings: Add application.can_use__application_as_host ReverseNavigation [Thodoris Greasidis] 994 | > * Add missing apiKey.getDeviceApiKeysByDevice docs [Thodoris Greasidis] 995 | > 996 | > ### balena-sdk-15.57.0 - 2021-11-05 997 | > 998 | > * models/api-key: Change update() & revoke() to work with all key variants [Thodoris Greasidis] 999 | > 1000 | > ### balena-sdk-15.56.0 - 2021-11-04 1001 | > 1002 | > * models/apiKey: Add getDeviceApiKeysByDevice() method [Thodoris Greasidis] 1003 | > 1004 | > ### balena-sdk-15.55.0 - 2021-11-01 1005 | > 1006 | > * typings: Add the release.raw_version property [Thodoris Greasidis] 1007 | > 1008 | > ### balena-sdk-15.54.2 - 2021-10-25 1009 | > 1010 | > * application/create: Rely on the hostApps for detecting discontinued DTs [Thodoris Greasidis] 1011 | > 1012 | > ### balena-sdk-15.54.1 - 2021-10-22 1013 | > 1014 | > * tests/device: Async-await conversions & abstraction on multi-field tests [Thodoris Greasidis] 1015 | > 1016 | > ### balena-sdk-15.54.0 - 2021-10-20 1017 | > 1018 | > * tests: Register devices in chunks of 10 to avoid uuid conflicts in node [Thodoris Greasidis] 1019 | > * Add known issue check on release isReccomanded logic [JSReds] 1020 | > * Add known_issue_list to hostApp.getOsVersions() [JSReds] 1021 | > 1022 | > ### balena-sdk-15.53.0 - 2021-10-07 1023 | > 1024 | > * Add support for batch device supervisor updates [Thodoris Greasidis] 1025 | > 1026 | > ### balena-sdk-15.52.0 - 2021-10-06 1027 | > 1028 | > * Add support for batch device pinning to release [Thodoris Greasidis] 1029 | > 1030 | > ### balena-sdk-15.51.4 - 2021-09-28 1031 | > 1032 | > * auth.isLoggedIn: Treat BalenaExpiredToken errors as logged out indicator [Thodoris Greasidis] 1033 | > 1034 | > ### balena-sdk-15.51.3 - 2021-09-28 1035 | > 1036 | > * Convert application spec to TypeScript [Thodoris Greasidis] 1037 | > 1038 | > ### balena-sdk-15.51.2 - 2021-09-28 1039 | > 1040 | > * application.trackLatestRelease: Fix using draft/invalidated releases [Thodoris Greasidis] 1041 | > * application.isTrackingLatestRelease: Exclude draft&invalidated releases [Thodoris Greasidis] 1042 | > 1043 | > ### balena-sdk-15.51.1 - 2021-09-20 1044 | > 1045 | > 1046 | >
1047 | > Update balena-request to v11.4.2 [Kyle Harding] 1048 | > 1049 | >> #### balena-request-11.4.2 - 2021-09-20 1050 | >> 1051 | >> * Allow overriding the default zlib flush setting [Kyle Harding] 1052 | >> 1053 | >
1054 | > 1055 | > 1056 | > ### balena-sdk-15.51.0 - 2021-09-16 1057 | > 1058 | > * os.getConfig: Add typings for the provisioningKeyName option [Nitish Agarwal] 1059 | > 1060 | > ### balena-sdk-15.50.1 - 2021-09-13 1061 | > 1062 | > * models/os: Always first normalize the device type slug [Thodoris Greasidis] 1063 | > 1064 | > ### balena-sdk-15.50.0 - 2021-09-10 1065 | > 1066 | > * Add release.finalize to promote draft releases to final [toochevere] 1067 | > 1068 | > ### balena-sdk-15.49.1 - 2021-09-10 1069 | > 1070 | > * typings: Drop the v5-model-only application_type.is_host_os [Thodoris Greasidis] 1071 | > 1072 | > ### balena-sdk-15.49.0 - 2021-09-06 1073 | > 1074 | > * os.getSupportedOsUpdateVersions: Use the hostApp releases [Thodoris Greasidis] 1075 | > * os.download: Use the hostApp for finding the latest release [Thodoris Greasidis] 1076 | > 1077 | > ### balena-sdk-15.48.3 - 2021-08-27 1078 | > 1079 | > 1080 | >
1081 | > Update balena-request to 11.4.1 [Kyle Harding] 1082 | > 1083 | >> #### balena-request-11.4.1 - 2021-08-27 1084 | >> 1085 | >> * Allow more lenient gzip decompression [Kyle Harding] 1086 | >> 1087 | >
1088 | > 1089 | > 1090 | > ### balena-sdk-15.48.2 - 2021-08-27 1091 | > 1092 | > * Improve hostapp.getAllOsVersions performance & reduce fetched data [Thodoris Greasidis] 1093 | > 1094 | > ### balena-sdk-15.48.1 - 2021-08-27 1095 | > 1096 | > * Update typescript to 4.4.2 [Thodoris Greasidis] 1097 | > 1098 | > ### balena-sdk-15.48.0 - 2021-08-15 1099 | > 1100 | > * Deprecate the release.release_version property [Thodoris Greasidis] 1101 | > * typings: Add the release versioning properties [Thodoris Greasidis] 1102 | > 1103 | > ### balena-sdk-15.47.1 - 2021-08-10 1104 | > 1105 | > * Run browser tests using the minified browser bundle [Thodoris Greasidis] 1106 | > * Move to uglify-js to fix const assignment bug in minified build [Thodoris Greasidis] 1107 | > 1108 | > ### balena-sdk-15.47.0 - 2021-08-09 1109 | > 1110 | > * typings: Add the release.is_final & is_finalized_at__date properties [Thodoris Greasidis] 1111 | > 1112 | > ### balena-sdk-15.46.1 - 2021-07-28 1113 | > 1114 | > * apiKey.getAll: Return only NamedUserApiKeys for backwards compatibility [Thodoris Greasidis] 1115 | > 1116 | > ### balena-sdk-15.46.0 - 2021-07-27 1117 | > 1118 | > * Add email verification & email request methods [Nitish Agarwal] 1119 | > 1120 | > ### balena-sdk-15.45.0 - 2021-07-26 1121 | > 1122 | > * Update generateProvisioningKey to include keyName [Nitish Agarwal] 1123 | > 1124 |
1125 | 1126 | ## 11.0.0 - 2021-10-13 1127 | 1128 | * Avoid creating multiple preload containers [Kyle Harding] 1129 | * major: Remove balena-preload script in favor of use with CLI [Lorenzo Alberto Maria Ambrosi] 1130 | * Fix missing 'await' for getEdisonPartitions() [Paulo Castro] 1131 | * Add extra type information (refactor bind mount array) [Paulo Castro] 1132 | * Run linter [Paulo Castro] 1133 | * major: Convert to typescript [Lorenzo Alberto Maria Ambrosi] 1134 | * patch: Fix incorrect python List index check [Lorenzo Alberto Maria Ambrosi] 1135 | 1136 | ## 10.5.0 - 2021-08-04 1137 | 1138 | * Remove mutually exclusive args from sfdisk [Kyle Harding] 1139 | * Explicitly disable tls to avoid startup delays [Kyle Harding] 1140 | * Use custom dind image based on alpine [Kyle Harding] 1141 | 1142 | ## 10.4.20 - 2021-07-26 1143 | 1144 | * Avoid TypeError if build output array is empty [Kyle Harding] 1145 | 1146 | ## 10.4.19 - 2021-07-23 1147 | 1148 | * Start fetching image info sooner and run in parallel with api calls [Pagan Gazzard] 1149 | 1150 | ## 10.4.18 - 2021-07-23 1151 | 1152 | * Fix missing await [Pagan Gazzard] 1153 | 1154 | ## 10.4.17 - 2021-07-23 1155 | 1156 | * Use class syntax for declaring instance variables [Pagan Gazzard] 1157 | 1158 | ## 10.4.16 - 2021-07-22 1159 | 1160 | * Catch errors during build of preload image [Kyle Harding] 1161 | 1162 | ## 10.4.15 - 2021-07-22 1163 | 1164 | * Make use of async/await to simplify the code [Pagan Gazzard] 1165 | 1166 | ## 10.4.14 - 2021-07-22 1167 | 1168 | * Remove unnecessary fetching of balena settings and token [Pagan Gazzard] 1169 | 1170 | ## 10.4.13 - 2021-07-22 1171 | 1172 | * Switch to tmp-promise instead of tmp [Pagan Gazzard] 1173 | 1174 | ## 10.4.12 - 2021-07-22 1175 | 1176 | * Prefer `this.appId` to `this.application.id` [Pagan Gazzard] 1177 | 1178 | ## 10.4.11 - 2021-07-22 1179 | 1180 | * Update balena-lint to 6.x, typescript to 4.x [Pagan Gazzard] 1181 | 1182 | ## 10.4.10 - 2021-07-22 1183 | 1184 | * Support new storage-driver location in balena.service [Kyle Harding] 1185 | 1186 | ## 10.4.9 - 2021-06-28 1187 | 1188 | * Fix supervisor repository regex [Alex Gonzalez] 1189 | 1190 | ## 10.4.8 - 2021-06-23 1191 | 1192 | * Delay populating global partitions cache until methods call [Kyle Harding] 1193 | 1194 | ## 10.4.7 - 2021-05-17 1195 | 1196 | * CI: limit tests to one platform since we are only linting [Kyle Harding] 1197 | * Fixup linting by disabling editorconfig for .js files [Kyle Harding] 1198 | * Avoid hardcoded registry2 url [Kyle Harding] 1199 | 1200 | ## 10.4.6 - 2021-05-06 1201 | 1202 | * Update dependencies (dockerode, docker-progress) [Paulo Castro] 1203 | * Don't assume that 'docker' argument uses Bluebird promises [Paulo Castro] 1204 | 1205 | ## 10.4.5 - 2021-05-05 1206 | 1207 | * Enhance comms between CLI process and Python process in container [Paulo Castro] 1208 | 1209 | ## 10.4.4 - 2021-05-05 1210 | 1211 | * Fix unhandled exception on container.wait() [Paulo Castro] 1212 | * Emit error on container status code '137' too (OOM SIGKILL) [Paulo Castro] 1213 | * Add flake8 npm script [Paulo Castro] 1214 | 1215 | ## 10.4.3 - 2021-05-04 1216 | 1217 | * Don't truncate error logs (add _truncate_exc option to python sh commands) [Paulo Castro] 1218 | * Add keyword args to RetryCounter key computation. Remove unused args. [Paulo Castro] 1219 | 1220 | ## 10.4.2 - 2021-04-26 1221 | 1222 | * Bump Docker version to 20.10.6 [Kyle Harding] 1223 | 1224 | ## 10.4.1 - 2020-12-28 1225 | 1226 | * Store temporary docker pull files in a separate volume [Alexis Svinartchouk] 1227 | * Round the additional required bytes [Alexis Svinartchouk] 1228 | 1229 | ## 10.4.0 - 2020-09-24 1230 | 1231 | * Add additionalSpace option to override additional space calculation [Alexis Svinartchouk] 1232 | * Sum unique layers sizes instead of image sizes for size estimation [Alexis Svinartchouk] 1233 | 1234 | ## 10.3.1 - 2020-08-26 1235 | 1236 | * Fix splash image file name for balenaOS >= 2.53.0 (resin -> balena) [Paulo Castro] 1237 | * Don't produce (and ignore) a package-lock.json file [Paulo Castro] 1238 | 1239 | ## 10.3.0 - 2020-08-17 1240 | 1241 | 1242 |
1243 | Update docker-progress to 4.x [Pagan Gazzard] 1244 | 1245 | > ### docker-progress-4.0.3 - 2020-08-17 1246 | > 1247 | > * Update to balena-lint 5.x [Pagan Gazzard] 1248 | > 1249 | > ### docker-progress-4.0.2 - 2020-08-17 1250 | > 1251 | > * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard] 1252 | > 1253 | > ### docker-progress-4.0.1 - 2020-03-04 1254 | > 1255 | > * Update dependencies [Pagan Gazzard] 1256 | > 1257 | > ### docker-progress-4.0.0 - 2019-03-26 1258 | > 1259 | > * Detect error events in push/pull progress streams [Paulo Castro] 1260 | > 1261 |
1262 | 1263 | ## 10.2.4 - 2020-08-10 1264 | 1265 | * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard] 1266 | 1267 | ## 10.2.3 - 2020-08-04 1268 | 1269 | * Fix build dir paths [Pagan Gazzard] 1270 | 1271 | ## 10.2.2 - 2020-08-04 1272 | 1273 | 1274 |
1275 | Update balena-sdk to 15.x [Pagan Gazzard] 1276 | 1277 | > ### balena-sdk-15.2.1 - 2020-08-03 1278 | > 1279 | > * Convert majority to async/await [Pagan Gazzard] 1280 | > 1281 | > ### balena-sdk-15.2.0 - 2020-07-31 1282 | > 1283 | > * device: add method to update target supervisor release [Matthew McGinn] 1284 | > 1285 | > ### balena-sdk-15.1.1 - 2020-07-27 1286 | > 1287 | > * Deduplicate device update methods [Pagan Gazzard] 1288 | > 1289 | > ### balena-sdk-15.1.0 - 2020-07-27 1290 | > 1291 | > 1292 | >
1293 | > Update balena-pine to add support for and make use of named keys [Pagan Gazzard] 1294 | > 1295 | >> #### balena-pine-12.2.0 - 2020-07-22 1296 | >> 1297 | >> 1298 | >>
1299 | >> Update pinejs-client-core [Pagan Gazzard] 1300 | >> 1301 | >>> ##### pinejs-client-js-6.1.0 - 2020-07-21 1302 | >>> 1303 | >>> * Add support for using named ids [Pagan Gazzard] 1304 | >>> 1305 | >>
1306 | >> 1307 | >> 1308 | >> #### balena-request-11.1.0 - 2020-07-16 1309 | >> 1310 | >> * Add lazy loading for most modules [Pagan Gazzard] 1311 | >> 1312 | >
1313 | > 1314 | > 1315 | > ### balena-sdk-15.0.3 - 2020-07-27 1316 | > 1317 | > * typings: Fix the PineWithSelect & related type helpers [Thodoris Greasidis] 1318 | > * typings: Use the native TypeScript Omit type helper [Thodoris Greasidis] 1319 | > 1320 | > ### balena-sdk-15.0.2 - 2020-07-22 1321 | > 1322 | > * Fix code snippet for initializing balena-sdk [Vipul Gupta (@vipulgupta2048)] 1323 | > 1324 | > ### balena-sdk-15.0.1 - 2020-07-15 1325 | > 1326 | > * Fix SupportTier/includes__SLA typing [Pagan Gazzard] 1327 | > 1328 | > ### balena-sdk-15.0.0 - 2020-07-15 1329 | > 1330 | > * **BREAKING** Export setSharedOptions & fromSharedOptions separately [Thodoris Greasidis] 1331 | > * **BREAKING** Export as an ES6 module [Thodoris Greasidis] 1332 | > 1333 | >
1334 | > Update dependencies and switch all returned promises to native promises [Pagan Gazzard] 1335 | > 1336 | >> #### balena-auth-4.0.2 - 2020-07-13 1337 | >> 1338 | >> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard] 1339 | >> 1340 | >> #### balena-auth-4.0.1 - 2020-07-03 1341 | >> 1342 | >> * Explicitly add tslib dependency [Pagan Gazzard] 1343 | >> 1344 | >> #### balena-auth-4.0.0 - 2020-07-02 1345 | >> 1346 | >> * Update to balena-settings-storage 6.x [Pagan Gazzard] 1347 | >> * Update target to es2015 [Pagan Gazzard] 1348 | >> * Switch to native promises [Pagan Gazzard] 1349 | >> * Enable strict type checking [Pagan Gazzard] 1350 | >> * Specify node 10+ [Pagan Gazzard] 1351 | >> 1352 | >> #### balena-auth-3.1.1 - 2020-07-02 1353 | >> 1354 | >> * Switch to @balena/lint for linting [Pagan Gazzard] 1355 | >> 1356 | >> #### balena-pine-12.1.1 - 2020-07-13 1357 | >> 1358 | >> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard] 1359 | >> 1360 | >> #### balena-pine-12.1.0 - 2020-07-06 1361 | >> 1362 | >> * Update balena-auth to 4.x and balena-request to 11.x [Pagan Gazzard] 1363 | >> 1364 | >> #### balena-pine-12.0.1 - 2020-07-03 1365 | >> 1366 | >> * Use typescript import helpers [Pagan Gazzard] 1367 | >> 1368 | >> #### balena-pine-12.0.0 - 2020-06-26 1369 | >> 1370 | >> * Stop actively supporting node 8 [Thodoris Greasidis] 1371 | >> * Convert to async await [Thodoris Greasidis] 1372 | >> * Add balenaCI repo.yml [Thodoris Greasidis] 1373 | >> * karma.conf.js: Combine declaration & assignment of karmaConfig [Thodoris Greasidis] 1374 | >> * Bump @balena/lint to v5 [Thodoris Greasidis] 1375 | >> * Drop getPine() in favor of an es6 export of the BalenaPine class [Thodoris Greasidis] 1376 | >> * Drop the API_PREFIX property in favor of the apiPrefix [Thodoris Greasidis] 1377 | >> * Bump to pinejs-client v6 which requires es2015 & drops Bluebird promises [Thodoris Greasidis] 1378 | >> 1379 | >> #### balena-pine-11.2.1 - 2020-06-15 1380 | >> 1381 | >> * Convert karma.conf to js [Thodoris Greasidis] 1382 | >> * Bump balena-config-karma to v3 [Thodoris Greasidis] 1383 | >> 1384 | >> #### balena-register-device-7.1.0 - 2020-07-13 1385 | >> 1386 | >> * Switch from randomstring to uuid for generating device uuids [Pagan Gazzard] 1387 | >> 1388 | >> #### balena-register-device-7.0.1 - 2020-07-13 1389 | >> 1390 | >> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard] 1391 | >> 1392 | >> #### balena-register-device-7.0.0 - 2020-07-06 1393 | >> 1394 | >> * Convert to type checked javascript [Pagan Gazzard] 1395 | >> * Drop callback interface in favor of promise interface [Pagan Gazzard] 1396 | >> * Switch to a named export [Pagan Gazzard] 1397 | >> * Convert to typescript [Pagan Gazzard] 1398 | >> * Update to typed-error 3.x [Pagan Gazzard] 1399 | >> * Switch to returning native promises [Pagan Gazzard] 1400 | >> * Update to balena-request 11.x [Pagan Gazzard] 1401 | >> * Use typescript import helpers [Pagan Gazzard] 1402 | >> 1403 | >> #### balena-register-device-6.1.6 - 2020-05-26 1404 | >> 1405 | >> * Export ApiError [Cameron Diver] 1406 | >> 1407 | >> #### balena-register-device-6.1.5 - 2020-05-21 1408 | >> 1409 | >> * Convert tests to js [Thodoris Greasidis] 1410 | >> 1411 | >> #### balena-register-device-6.1.4 - 2020-05-21 1412 | >> 1413 | >> * Install typed-error v2 [Cameron Diver] 1414 | >> 1415 | >> #### balena-register-device-6.1.3 - 2020-05-20 1416 | >> 1417 | >> * Extend API exception to include full response object [Miguel Casqueira] 1418 | >> 1419 | >> #### balena-register-device-6.1.2 - 2020-05-20 1420 | >> 1421 | >> * Update mocha to fix node v12 deprecation warning [Thodoris Greasidis] 1422 | >> 1423 | >> #### balena-request-11.0.4 - 2020-07-14 1424 | >> 1425 | >> * Fix body overwriting on nodejs [Pagan Gazzard] 1426 | >> 1427 | >> #### balena-request-11.0.3 - 2020-07-13 1428 | >> 1429 | >> * Add .versionbot/CHANGELOG.yml for nested changelogs [Pagan Gazzard] 1430 | >> 1431 | >> #### balena-request-11.0.2 - 2020-07-06 1432 | >> 1433 | >> * Fix tslib dependency [Pagan Gazzard] 1434 | >> 1435 | >> #### balena-request-11.0.1 - 2020-07-03 1436 | >> 1437 | >> * Fix passing baseUrl to refreshToken if the request uses an absolute url [Pagan Gazzard] 1438 | >> 1439 | >> #### balena-request-11.0.0 - 2020-07-03 1440 | >> 1441 | >> * Convert to type checked javascript [Pagan Gazzard] 1442 | >> * Switch to returning native promises [Pagan Gazzard] 1443 | >> * Drop support for nodejs < 10 [Pagan Gazzard] 1444 | >> * Update balena-auth to 4.x [Pagan Gazzard] 1445 | >> * Remove rindle dependency [Pagan Gazzard] 1446 | >> * Update fetch-ponyfill to 6.x [Pagan Gazzard] 1447 | >> * Remove proxy tests as global-tunnel-ng only supports nodejs < 10.16.0 [Pagan Gazzard] 1448 | >> * Switch to a named export [Pagan Gazzard] 1449 | >> * Use typescript import helpers [Pagan Gazzard] 1450 | >> * Bump balena-config-karma & convert karma.conf.coffee to js [Thodoris Greasidis] 1451 | >> * Change the browser request timeout error to be consistent with node [Thodoris Greasidis] 1452 | >> 1453 | >
1454 | > 1455 | > * **BREAKING** billing: Make the organization parameter fist & required [Thodoris Greasidis] 1456 | > 1457 | > ### balena-sdk-14.8.0 - 2020-07-15 1458 | > 1459 | > * DeviceWithServiceDetails: preserve the image_install & gateway_downloads [Thodoris Greasidis] 1460 | > * typings: Deprecate DeviceWithImageInstalls in favor of the Device type [Thodoris Greasidis] 1461 | > 1462 | > ### balena-sdk-14.7.1 - 2020-07-14 1463 | > 1464 | > * Fix is_private typings for device type [Stevche Radevski] 1465 | > 1466 | > ### balena-sdk-14.7.0 - 2020-07-14 1467 | > 1468 | > * Add an organization parameter to all billing methods [Thodoris Greasidis] 1469 | > 1470 | > ### balena-sdk-14.6.0 - 2020-07-13 1471 | > 1472 | > * typings: Add ApplicationHostedOnApplication [Thodoris Greasidis] 1473 | > * typings Add RecoveryTwoFactor [Thodoris Greasidis] 1474 | > 1475 | > ### balena-sdk-14.5.1 - 2020-07-10 1476 | > 1477 | > * Tests: remove bluebird usage [Pagan Gazzard] 1478 | > 1479 | > ### balena-sdk-14.5.0 - 2020-07-09 1480 | > 1481 | > * tests/integration/setup: Convert to TypeScript [Thodoris Greasidis] 1482 | > * typings/ImageInstall: Deprecate the image field [Thodoris Greasidis] 1483 | > * typings/ImageInstall: Add the `installs__image` field [Thodoris Greasidis] 1484 | > * typings: Add typings for the ReleaseImage [Thodoris Greasidis] 1485 | > * typings/ImageInstall: Add the missing device property [Thodoris Greasidis] 1486 | > * Convert all remaining tests away from coffeescript [Pagan Gazzard] 1487 | > 1488 | > ### balena-sdk-14.4.2 - 2020-07-09 1489 | > 1490 | > * Tests: improve typing for access to private SDK os methods [Pagan Gazzard] 1491 | > * Tests: improve typing of tag helpers [Pagan Gazzard] 1492 | > * Tests: import BalenaSDK types directly [Pagan Gazzard] 1493 | > 1494 | > ### balena-sdk-14.4.1 - 2020-07-08 1495 | > 1496 | > * Tests: merge multiple application deletions into a single call [Pagan Gazzard] 1497 | > 1498 | > ### balena-sdk-14.4.0 - 2020-07-08 1499 | > 1500 | > * Improve typings for `sdk.pine.post` [Pagan Gazzard] 1501 | > * Improve typings for `sdk.request` [Pagan Gazzard] 1502 | > * Improve typings for `models.device.getOsVersion` [Pagan Gazzard] 1503 | > * Improve typings for `models.device.lastOnline` [Pagan Gazzard] 1504 | > * Fix typings for `models.device.getMACAddresses` [Pagan Gazzard] 1505 | > * Fix typings for `models.device.getLocalIPAddresses` [Pagan Gazzard] 1506 | > * Add typings for `models.application.getDashboardUrl` [Pagan Gazzard] 1507 | > * Device model: last_connectivity_event and os_version can be null [Pagan Gazzard] 1508 | > * Improve typings for `models.device.getLocalModeSupport` [Pagan Gazzard] 1509 | > 1510 | > ### balena-sdk-14.3.3 - 2020-07-07 1511 | > 1512 | > * Minimize bluebird sugar usage [Pagan Gazzard] 1513 | > 1514 | > ### balena-sdk-14.3.2 - 2020-07-07 1515 | > 1516 | > * Add type checking for tests [Pagan Gazzard] 1517 | > 1518 | > ### balena-sdk-14.3.1 - 2020-07-07 1519 | > 1520 | > * Tests: cache device type lookup [Pagan Gazzard] 1521 | > 1522 | > ### balena-sdk-14.3.0 - 2020-07-07 1523 | > 1524 | > * typings: Export pine variant w/ a mandatory $select on get requests [Thodoris Greasidis] 1525 | > 1526 | > ### balena-sdk-14.2.9 - 2020-07-07 1527 | > 1528 | > * Remove `this.skip` usage as a faster workaround to afterEach skipping [Pagan Gazzard] 1529 | > 1530 | > ### balena-sdk-14.2.8 - 2020-07-06 1531 | > 1532 | > * Improve internal typings by avoiding some `any` cases [Pagan Gazzard] 1533 | > 1534 | > ### balena-sdk-14.2.7 - 2020-07-06 1535 | > 1536 | > * Include typings for all lazy loaded requires [Pagan Gazzard] 1537 | > 1538 | > ### balena-sdk-14.2.6 - 2020-07-06 1539 | > 1540 | > * Simplify balena-request custom typings [Pagan Gazzard] 1541 | > * Use import type for declaration imports [Pagan Gazzard] 1542 | > * Simplify balena-pine custom typings [Pagan Gazzard] 1543 | > * Import balena-sdk type declarations via import type and not direct path [Pagan Gazzard] 1544 | > 1545 | > ### balena-sdk-14.2.5 - 2020-07-06 1546 | > 1547 | > * Use typescript import helpers [Pagan Gazzard] 1548 | > 1549 | > ### balena-sdk-14.2.4 - 2020-07-03 1550 | > 1551 | > * Drop dtslint in favor of plain @ts-expect-error [Thodoris Greasidis] 1552 | > * Enable strict checks for the typing tests [Thodoris Greasidis] 1553 | > 1554 | > ### balena-sdk-14.2.3 - 2020-07-03 1555 | > 1556 | > * Standardize bluebird naming as `Bluebird` [Pagan Gazzard] 1557 | > 1558 | > ### balena-sdk-14.2.2 - 2020-07-03 1559 | > 1560 | > * Avoid $ExpectType b/c of issues with TS 3.9.6 [Thodoris Greasidis] 1561 | > 1562 | > ### balena-sdk-14.2.1 - 2020-07-01 1563 | > 1564 | > * model: Add build_environment_variable [Rich Bayliss] 1565 | > 1566 | > ### balena-sdk-14.2.0 - 2020-07-01 1567 | > 1568 | > * Add typings for plans & subscriptions [Thodoris Greasidis] 1569 | > 1570 | > ### balena-sdk-14.1.0 - 2020-06-29 1571 | > 1572 | > * Generate optional build for es2018 as well as the default es2015 [Pagan Gazzard] 1573 | > 1574 | > ### balena-sdk-14.0.2 - 2020-06-28 1575 | > 1576 | > * typings: Split the DeviceState namespace types to a different file [Thodoris Greasidis] 1577 | > * typings: Split the DeviceTypeJson namespace types to a different file [Thodoris Greasidis] 1578 | > * typings: Split the SBVR model types to a different file [Thodoris Greasidis] 1579 | > 1580 | > ### balena-sdk-14.0.1 - 2020-06-15 1581 | > 1582 | > * appveyor: Increase the node space size [Thodoris Greasidis] 1583 | > * Bump balena-config-karma to v3 [Thodoris Greasidis] 1584 | > 1585 |
1586 | 1587 | # v10.2.1 1588 | ## (2020-08-04) 1589 | 1590 | * Use @balena/lint for linting [Pagan Gazzard] 1591 | * Add type checking [Pagan Gazzard] 1592 | 1593 | # v10.2.0 1594 | ## (2020-07-15) 1595 | 1596 | * Add 'setAppIdAndCommit()' method to help decouple balena-cli and balena-preload [Paulo Castro] 1597 | * Increase losetup retry delay and count and print hint message on error [Paulo Castro] 1598 | * Print full stderr in the event of failure executing dockerd [Paulo Castro] 1599 | 1600 | # v10.1.1 1601 | ## (2020-07-10) 1602 | 1603 | * Flake8 on src/preload.py [Alexis Svinartchouk] 1604 | 1605 | # v10.1.0 1606 | ## (2020-07-08) 1607 | 1608 | * Add balena-sdk as a peer dependency since DI is also supported [Thodoris Greasidis] 1609 | * Make the balena-sdk instance an optional parameter [Thodoris Greasidis] 1610 | 1611 | # v10.0.0 1612 | ## (2020-07-01) 1613 | 1614 | * Update to balena-sdk 14.x [Thodoris Greasidis] 1615 | 1616 | # v9.0.0 1617 | ## (2020-06-30) 1618 | 1619 | * Update to balena-sdk 13.x [Pagan Gazzard] 1620 | 1621 | ## 8.4.0 - 2020-03-04 1622 | 1623 | * Update dependencies [Pagan Gazzard] 1624 | 1625 | ## 8.3.1 - 2020-02-05 1626 | 1627 | * Check os/app architecture compatibility rather than equvalence. [Scott Lowe] 1628 | * Update .gitignore [Scott Lowe] 1629 | 1630 | ## 8.3.0 - 2020-01-24 1631 | 1632 | * Update dependencies [Pagan Gazzard] 1633 | 1634 | ## 8.2.2 - 2019-12-17 1635 | 1636 | * Upgrade docker version to 17.12.0-ce [Theodor Gherzan] 1637 | 1638 | ## 8.2.1 - 2019-07-18 1639 | 1640 | * Fix certificates path in the preloader container [Alexis Svinartchouk] 1641 | 1642 | ## 8.2.0 - 2019-05-29 1643 | 1644 | * Add --add-certificate option [Alexis Svinartchouk] 1645 | * Stop mounting host's /etc/ssl/certs on Linux [Alexis Svinartchouk] 1646 | * Replace tar-stream with tar-fs [Alexis Svinartchouk] 1647 | 1648 | ## 8.1.4 - 2019-05-21 1649 | 1650 | * Fetch the app commit if 'latest' is passed [Alexis Svinartchouk] 1651 | 1652 | ## 8.1.3 - 2019-05-15 1653 | 1654 | * Use more robust regex to extract supervisor semver [Gergely Imreh] 1655 | 1656 | ## 8.1.2 - 2019-05-09 1657 | 1658 | * Filter by commit as part of the query if it's specified [Pagan Gazzard] 1659 | 1660 | ## 8.1.1 - 2019-02-18 1661 | 1662 | * Don't write device name in apps.json [Alexis Svinartchouk] 1663 | 1664 | ## 8.1.0 - 2019-01-07 1665 | 1666 | * Bind mount host's /etc/ssl/certs folder on Linux [Alexis Svinartchouk] 1667 | 1668 | ## 8.0.4 - 2018-11-15 1669 | 1670 | * Don't try to get the device type arch with --dontCheckArch [Alexis Svinartchouk] 1671 | 1672 | ## 8.0.3 - 2018-11-13 1673 | 1674 | * The splash image is named resin-logo.png [Alexis Svinartchouk] 1675 | 1676 | ## 8.0.2 - 2018-11-13 1677 | 1678 | * Don't try to pin releases when supervisor is < 7.0.0 [Alexis Svinartchouk] 1679 | 1680 | ## 8.0.1 - 2018-11-08 1681 | 1682 | * Bind mount host's /dev to container's /dev [Alexis Svinartchouk] 1683 | 1684 | ## v8.0.0 - 2018-10-29 1685 | 1686 | * Rename to balena-preload [Alexis Svinartchouk] 1687 | 1688 | ## v7.0.6 - 2018-10-29 1689 | 1690 | * Don't build the Dockerfile on CI [Alexis Svinartchouk] 1691 | 1692 | ## v7.0.5 - 2018-09-06 1693 | 1694 | * Filter out serviceId for supervisors < 7 [Alexis Svinartchouk] 1695 | 1696 | ## v7.0.4 - 2018-08-31 1697 | 1698 | * Get the release ID not the application ID [Theodor Gherzan] 1699 | 1700 | ## v7.0.3 - 2018-08-29 1701 | 1702 | * Use localhost instead of 0.0.0.0 to connect to Docker on win32 [Alexis Svinartchouk] 1703 | 1704 | ## v7.0.2 - 2018-08-28 1705 | 1706 | * Preload.py: Flush output buffer when data's ready #183 [Gergely Imreh] 1707 | * Dockerfile: Do not rely on script being executable #183 [Gergely Imreh] 1708 | 1709 | ## v7.0.1 - 2018-08-21 1710 | 1711 | * Pinning information should also be available for the resin-image-flasher #182 [Theodor Gherzan] 1712 | 1713 | ## v7.0.0 - 2018-08-20 1714 | 1715 | * Check device architecture instead of device type #181 [Alexis Svinartchouk] 1716 | 1717 | ## v6.3.2 - 2018-08-09 1718 | 1719 | * In the case of a flasher OS copy splash image to its boot partition #178 [Theodor Gherzan] 1720 | 1721 | ## v6.3.1 - 2018-05-30 1722 | 1723 | * Increase size multiplier from 1.1 to 1.4 #177 [Alexis Svinartchouk] 1724 | 1725 | ## v6.3.0 - 2018-05-30 1726 | 1727 | * Add circleci auto-deploy to npm #175 [Cameron Diver] 1728 | * Allow devices to be pinned following a preload provision #175 [Cameron Diver] 1729 | 1730 | ## v6.2.0 - 2018-03-23 1731 | 1732 | * Update resin-sdk to 9.0.0-beta16, use $ before expand #171 [Alexis Svinartchouk] 1733 | 1734 | ## v6.1.2 - 2018-03-20 1735 | 1736 | * Add appId to each app in apps.json for supertvisors < 7 (apps.json is from state v1) #169 [Alexis Svinartchouk] 1737 | 1738 | ## v6.1.1 - 2018-03-19 1739 | 1740 | * Fix preloading images with supervisor version < 7 #167 [Alexis Svinartchouk] 1741 | 1742 | ## v6.1.0 - 2018-03-19 1743 | 1744 | * Preload the release with the highest id if an app has no commit specified. #165 [Alexis Svinartchouk] 1745 | * Use state v1 endpoint when the supervisor version is < 7 #165 [Alexis Svinartchouk] 1746 | * Fix specifying commit #165 [Alexis Svinartchouk] 1747 | 1748 | ## v6.0.0 - 2018-03-09 1749 | 1750 | * Multicontainer preload #155 [Alexis Svinartchouk] 1751 | 1752 | ## v5.2.2 - 2018-02-28 1753 | 1754 | * Get_inner_image_path falls back to the first file in /opt if no deviceType.json file is found. #163 [Alexis Svinartchouk] 1755 | 1756 | ## v5.2.1 - 2018-02-22 1757 | 1758 | * Get the inner image name from device_type.json #161 [Alexis Svinartchouk] 1759 | 1760 | ## v5.2.0 - 2018-02-06 1761 | 1762 | * Preserve bootstrap code in MBR when resizing #158 [Alexis Svinartchouk] 1763 | 1764 | ## v5.1.2 - 2018-01-12 1765 | 1766 | * Bind mount /dev on linux hosts #153 [Alexis Svinartchouk] 1767 | 1768 | ## v5.1.1 - 2017-12-12 1769 | 1770 | * Remove all references to --dont-detect-flasher-type-images #151 [Alexis Svinartchouk] 1771 | * Minor fixes in preload.py #151 [Alexis Svinartchouk] 1772 | 1773 | ## v5.1.0 - 2017-12-12 1774 | 1775 | * Preload images that use Balena instead of Docker #149 [Alexis Svinartchouk] 1776 | 1777 | ## v5.0.4 - 2017-11-17 1778 | 1779 | * Wait for loop device to be detached before killing the container #147 [Alexis Svinartchouk] 1780 | 1781 | ## v5.0.3 - 2017-11-16 1782 | 1783 | * Fix confusion between bytes and sectors on partition resize #145 [Alexis Svinartchouk] 1784 | 1785 | ## v5.0.2 - 2017-11-16 1786 | 1787 | * Fix filesystem expansion after resize #143 [Alexis Svinartchouk] 1788 | 1789 | ## v5.0.1 - 2017-11-08 1790 | 1791 | * Update README: docker-toolbox is not supported, aufs module is required. #141 [Alexis Svinartchouk] 1792 | 1793 | ## v5.0.0 - 2017-10-26 1794 | 1795 | * Preload GPT disk images, remove --dont-detect-flasher-type-images option #138 [Alexis Svinartchouk] 1796 | 1797 | ## v4.0.9 - 2017-10-25 1798 | 1799 | * Don't set Preloader.imageRepo in setApplication. #137 [Alexis Svinartchouk] 1800 | 1801 | ## v4.0.8 - 2017-10-24 1802 | 1803 | * Fix incompatibility with Docker 1.12.6 (api version 1.24) #135 [Alexis Svinartchouk] 1804 | 1805 | ## v4.0.7 - 2017-10-24 1806 | 1807 | * Fix the --dont-detect-flasher-type-images option. #133 [Alexis Svinartchouk] 1808 | 1809 | ## v4.0.6 - 2017-10-20 1810 | 1811 | * Copy data between the MBR and the first partition when resizing a preloader image. #128 [Alexis Svinartchouk] 1812 | 1813 | ## v4.0.5 - 2017-10-20 1814 | 1815 | * Don't crash if data/docker/network/files/local-kv.db doesn't exist #130 [Alexis Svinartchouk] 1816 | 1817 | ## v4.0.4 - 2017-10-18 1818 | 1819 | * Fix preloading edison images 2.6+ (root partition filename changed) #126 [Alexis Svinartchouk] 1820 | * Support preloading 2.6+ resinOS images when hostapps use aufs or overlay2 #126 [Alexis Svinartchouk] 1821 | * Print a newline after each spinner #126 [Alexis Svinartchouk] 1822 | 1823 | ## v4.0.3 - 2017-10-18 1824 | 1825 | * Fix the logic that defines where we get R_OK and W_OK constants for node 4 support #125 [Pablo Carranza Velez] 1826 | 1827 | ## v4.0.2 - 2017-10-16 1828 | 1829 | * Get R_OK and W_OK constants from wherever they're available, to keep supporting node v4 for the time being #124 [Pablo Carranza Velez] 1830 | 1831 | ## v4.0.1 - 2017-10-13 1832 | 1833 | * Handle cases where we are not redirected when making a request to the registry. #122 [Alexis Svinartchouk] 1834 | 1835 | ## v4.0.0 - 2017-10-13 1836 | 1837 | * Allow preloading of Edison zip archives. #117 [Alexis Svinartchouk] 1838 | 1839 | ## v3.1.9 - 2017-10-06 1840 | 1841 | * Don't use Array.includes, it is not supported in node4. #119 [Alexis Svinartchouk] 1842 | 1843 | ## v3.1.8 - 2017-09-28 1844 | 1845 | * Use Docker bridged networking on platforms other than Linux. #111 [Alexis Svinartchouk] 1846 | 1847 | ## v3.1.7 - 2017-09-05 1848 | 1849 | * Don't use docker's PortBindings as we run it with `--net=host` #108 [Alexis Svinartchouk] 1850 | 1851 | ## v3.1.6 - 2017-08-31 1852 | 1853 | * Detect when the overlay module is not loaded and show an appropriate error. #105 [Alexis Svinartchouk] 1854 | 1855 | ## v3.1.5 - 2017-08-30 1856 | 1857 | * The python script no longer pulls the build, instead it exposes dockerd to the js script. This allows us to have a progress bar for the docker pull. #102 [Alexis Svinartchouk] 1858 | 1859 | ## v3.1.4 - 2017-08-25 1860 | 1861 | * Use the docker storage driver from //lib/systemd/system/docker.service ExecStart key. #96 [Alexis Svinartchouk] 1862 | 1863 | ## v3.1.3 - 2017-08-25 1864 | 1865 | * Fix js style (standard js) #100 [Alexis Svinartchouk] 1866 | 1867 | ## v3.1.2 - 2017-08-25 1868 | 1869 | * Add flag for ignoring mismatching device types [Sven Schwermer] 1870 | 1871 | ## v3.1.1 - 2017-08-24 1872 | 1873 | * Add a files section to package.json. #99 [Alexis Svinartchouk] 1874 | 1875 | ## v3.1.0 - 2017-08-22 1876 | 1877 | * Add progress bars and spinners. #95 [Alexis Svinartchouk] 1878 | 1879 | ## v3.0.0 - 2017-08-21 1880 | 1881 | * Don't preload the same build twice. #87 [Alexis Svinartchouk] 1882 | 1883 | ## v2.0.3 - 2017-08-21 1884 | 1885 | * Don't call stdin.setRawMode if it doesn't exist. #93 [Alexis Svinartchouk] 1886 | 1887 | ## v2.0.2 - 2017-08-18 1888 | 1889 | * Use only lower case for the preloaded image repository. #91 [Alexis Svinartchouk] 1890 | 1891 | ## v2.0.1 - 2017-08-16 1892 | 1893 | * Correctly set the resin api token. #85 [Alexis Svinartchouk] 1894 | 1895 | ## v2.0.0 - 2017-08-11 1896 | 1897 | * Integrate with resin-cli #81 [Alexis Svinartchouk] 1898 | 1899 | ## v1.4.1 - 2017-08-09 1900 | 1901 | * Use --net=host for host's Docker. #83 [Alexis Svinartchouk] 1902 | 1903 | ## v1.4.0 - 2017-08-04 1904 | 1905 | * Allow relative paths for the --image parameter. [Alexis Svinartchouk] 1906 | 1907 | ## v1.3.1 - 2017-08-04 1908 | 1909 | * Update the README about --dont-detect-flasher-type-images [Alexis Svinartchouk] 1910 | 1911 | ## v1.3.0 - 2017-08-04 1912 | 1913 | * Add a --dont-detect-flasher-type-images flag. [Alexis Svinartchouk] 1914 | 1915 | ## v1.2.2 - 2017-08-04 1916 | 1917 | * Use standardJS [Alexis Svinartchouk] 1918 | 1919 | ## v1.2.1 - 2017-07-27 1920 | 1921 | * Add CHANGELOG to allow VersionBot commits. [Alexis Svinartchouk] 1922 | 1923 | ## v1.2.0 - 2017-07-25 1924 | 1925 | * Allow specifying an app commit to preload. [Alexis Svinartchouk] 1926 | --------------------------------------------------------------------------------