-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
os.download: Fix balenaOS version format validation #1017
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,7 +30,7 @@ import type { | |
import { InjectedDependenciesParam, InjectedOptionsParam } from '..'; | ||
import { getAuthDependentMemoize } from '../util/cache'; | ||
|
||
const BALENAOS_VERSION_REGEX = /v?\d+\.\d+\.\d+(\.rev\d+)?((\-|\+).+)?/; | ||
const BALENAOS_VERSION_REGEX = /^\d+\.\d+\.\d+(\+rev\d+)?(\.(dev|prod))?$/; | ||
|
||
const getOsModel = function ( | ||
deps: InjectedDependenciesParam, | ||
|
@@ -159,7 +159,7 @@ const getOsModel = function ( | |
} | ||
const vNormalized = v[0] === 'v' ? v.substring(1) : v; | ||
if (!BALENAOS_VERSION_REGEX.test(vNormalized)) { | ||
throw new Error(`Invalid semver version: ${v}`); | ||
throw new Error(`Invalid balenaOS version format: ${v}`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change goes back to a point I raised in another comment, whether we mean to validate any generic semver version or specifically balenaOS versions. This change aligns with the latter. |
||
} | ||
return vNormalized; | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -450,6 +450,49 @@ describe('OS model', function () { | |
const promise = balena.models.os.download('foo-bar-baz'); | ||
return expect(promise).to.be.rejectedWith('No such device type'); | ||
})); | ||
|
||
describe('given well formed and malformed balenaOS versions', () => { | ||
it('should reject malformed versions with an error', async () => { | ||
const versions = [ | ||
'2.60.1+foo', | ||
'2.60.1-foo', | ||
'2.60.1.foo', | ||
'2.60.1+rev', | ||
'2.60.1+rev1+foo', | ||
'2.60.1+rev1-foo', | ||
'2.60.1+rev1.foo', | ||
'2.60.1+rev1.dev.foo', | ||
'2.60.1+rev1.prod.foo', | ||
]; | ||
for (const version of [...versions]) { | ||
versions.push(`v${version}`); | ||
} | ||
for (const version of versions) { | ||
await expect( | ||
balena.models.os.download('raspberry-pi', version), | ||
).to.be.rejectedWith(`Invalid balenaOS version format: ${version}`); | ||
} | ||
}); | ||
|
||
it('should accept well formed versions', async () => { | ||
const versions = [ | ||
'0.0.1', | ||
'0.0.1.dev', | ||
'0.0.1.prod', | ||
'0.0.1+rev1', | ||
'0.0.1+rev1.dev', | ||
'0.0.1+rev1.prod', | ||
]; | ||
for (const version of [...versions]) { | ||
versions.push(`v${version}`); | ||
} | ||
for (const version of versions) { | ||
await expect( | ||
balena.models.os.download('raspberry-pi', version), | ||
).to.be.rejectedWith(`No such version for the device type`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test works, but performs rather poorly: 5 seconds on my laptop. What I really wanted to test is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pdcastro Looks like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This might work in combination with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pdcastro you can move it to |
||
} | ||
}); | ||
}); | ||
}); | ||
|
||
describe('balena.models.os.isSupportedOsUpdate()', function () { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please double check this regex change:
(\.rev\d+)?
in the old regex seemed wrong to me, because we write balenaOS versions like2.60.1+rev1
rather than2.60.1.rev1
.((\-|\+).+)?
in the old regex might match prerelease versions as defined in the semver spec, and would match any strange thing like2.60.1-foo
. A question is whether this regex should verify any possible semver version as per semver spec, versus validating balenaOS versions as we know them. In this PR I have opted for the latter. If we wanted to match any possible semver spec, then the regex should not include therev\d+
part, which applies to balenaOS versions but not to generic semver versions.v?
because this regex is used afterv
removal (see code where the regex is used). If the leadingv?
was not removed, then a malformed version likevv2.60.1
would be accepted.^
at the beginning and$
at the end, to be stricter about the balenaOS version format.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pdcastro The issue is that we did have malformed semver os releases and that's why we originally added this here.
Imo we should ideally try to drop this semver regex and use
balena-semver
for validation instead.Even if balena-semver is looser, I expect that the API would anyway error if the version is wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thgreasi, on this point, my investigation didn't go far enough to provide an explanation, but actually the following CLI commands download an image (don't ask me what image, but an image is downloaded!), despite the versions being nonsense:
It's true though that semver validation (this PR) would not completely solve this problem, but the regex changes that I proposed would at least catch things like
-dev
instead of.dev
. Indeed, this was the motivation for this PR, originating from this other CLI PR by @codewithcheese: balena-io/balena-cli/pull/2084There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pdcastro it would be interesting to check the
os-version
in the downloaded images.It seems that the reason is that balena-image-manager is doing a version range check (which ends up using
semver.maxSatisfying
) which as a result.See: https://github.com/balena-io-modules/balena-image-manager/blob/2a4e905f7b125396303b69bff37bc901038c25d5/lib/manager.js#L92
See: https://github.com/balena-io-modules/balena-image-manager/blob/2a4e905f7b125396303b69bff37bc901038c25d5/lib/utils.js#L64-L65
Here are the respective test cases of the SDK method:
See:
balena-sdk/tests/integration/models/os.spec.ts
Lines 172 to 182 in a3ade4c
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behavior complicates the issue 🤔
The thing is that if you have a typo, you suddenly get the latest version matching just the major.minor.patch of what you provided, since anything after those doesn't count when sorting.
Just thinking out loud:
It might make sense to just clarify to the user which os version is getting downloaded if the final version doesn't match what they provided.
An alternative could be to only do range matching when only the major.minor.patch parts are provided, but this would stop
2.40.99+rev1
for successfully downloading an image, since the.prod
is missing.I think it makes sense to find all invalid os versions and confirm whether we still want to support them.
PS: The Classic app type requires version
>=2.0.3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having to use
etcher-sdk
to extract a specific file (/etc/os-release
) from a specific partition from a.img
image file, just because of the risk that the user specified a non-existent, even invalid version like2.60.1.foo
, sounds... Doable but a bit disheartening. :-)I like this suggestion! If a user adds a specific suffix like
+rev2
, then surely they don't mean a version range. 🤔 This also means that prefixes like~
or^
would not be compatible with a+rev
suffix. On the other hand,.dev
or.prod
are orthogonal to all this..dev
and.prod
are compatible with version ranges, and^2.60.1.dev
actually makes some sense (unlike^2.60.1+rev2
). This kind of validation would be beyond generic semver -- these are balenaOS specific validations, with specific meanings attached to specific suffixes.The SDK could automatically append
.prod
if neither.dev
not.prod
is provided. This PR could do it.How does one go about finding all available versions?
And: What about a SDK option to disable version validation? So that, by default, the malformed versions would raise an error, but CLI users would be given the option of adding
--novalidation
to disable validation.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually meant this by calling the respective method from the balena-image-manager (for sure not parsing the stream).
Update: I actually see that we already do print the version that end up being downloaded:
See: https://github.com/balena-io/balena-cli/blob/fa4f91e08d891b2f99f7e76aecb0ff7018d16ff4/lib/utils/cloud.ts#L146
I wouldn't expect this to work as any
.dev
after2.60.1
. Never seen any such semantics. Do we intend to add support for that? Imo it sounds like adding an extra variant filtering option to balena-image-manager would be better than extending balena-semver to allow such "filtered" range matching specifically for.dev
&.prod
(since that would diverge it even more from plain semver).@nazrhom / @sradevski can probably help you on that
Maybe we should drop this regex and just use balena-semver and always parse/validate the provided string.
Alternatively, instead of a
--novalidation
, I would prefer astrict/exact
argument that just throws a NotFoundError when a version with the exact string provided isn't available, than trying to validate the semver.Essentially this would fill the "gap" in the npm semver where there is no range symbol targeting the
build
part of a semver.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, this is an important comment. For whatever reason, I assumed that appending
.dev
to the OS version in a call to the SDK methodos.download(deviceType, [version])
was the way to request the download of a development image, as opposed to a production image. @codewithcheese even added this note to the output ofbalena help os download
in a recent CLI pull request (balena-io/balena-cli#2084).Question: what is the correct way of selecting a development image in a SDK call to
os.download(deviceType, [version])
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should actually hint the users to provide the exact version string (including the
.dev
build
part of the semver) as printed bybalena os versions <device_type>
.Yes, that's he one to use, which actually is the only download related method provided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh your question was about "how" not whether that's the one to use.
For the "how" part atm you have to pass the exact os version string eg:
os.download('raspberry-pi', '2.56.0+rev2.dev')
.Update: Actually if you are passing an argument other than
latest
toos.download
, then it has to be the full version string otherwise it erros.