-
Notifications
You must be signed in to change notification settings - Fork 285
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
RDX: Work with deployment profiles #4655
Changes from 3 commits
f82f365
1a92bbf
3cccdb1
2c1896c
8cdcbd9
2a8f22e
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 |
---|---|---|
@@ -0,0 +1,107 @@ | ||
load '../helpers/load' | ||
|
||
setup() { | ||
TESTDATA_DIR="${PATH_BATS_ROOT}/tests/extensions/testdata/" | ||
|
||
if using_windows_exe; then | ||
TESTDATA_DIR_CLI="$(wslpath -m "${TESTDATA_DIR}")" | ||
else | ||
TESTDATA_DIR_CLI="${TESTDATA_DIR}" | ||
fi | ||
|
||
if using_containerd; then | ||
namespace_arg=('--namespace=rancher-desktop-extensions') | ||
else | ||
namespace_arg=() | ||
fi | ||
} | ||
|
||
write_allow_list() { # list | ||
local list=${1-} | ||
local allowed=true | ||
|
||
if [ -z "$list" ]; then | ||
allowed=false | ||
fi | ||
|
||
# Note that the list preference is not writable using `rdctl set`, and we | ||
# need to do a direct API call instead. | ||
|
||
rdctl api /v1/settings --input - <<<'{ | ||
"version": '"$(get_setting .version)"', | ||
"application": { | ||
"extensions": { | ||
"allowed": { | ||
"enabled": '"${allowed}"', | ||
"list": '"${list:-[]}"' | ||
} | ||
} | ||
} | ||
}' | ||
} | ||
|
||
@test 'factory reset' { | ||
factory_reset | ||
} | ||
|
||
@test 'start container engine' { | ||
RD_ENV_EXTENSIONS=1 start_container_engine | ||
wait_for_container_engine | ||
} | ||
|
||
@test 'build extension testing image' { | ||
ctrctl "${namespace_arg[@]}" build \ | ||
--tag "rd/extension/basic" \ | ||
--build-arg "variant=basic" \ | ||
"$TESTDATA_DIR_CLI" | ||
|
||
run ctrctl "${namespace_arg[@]}" image list --format '{{ .Repository }}' | ||
assert_success | ||
assert_line "rd/extension/basic" | ||
} | ||
|
||
@test 'when no extension allow list is set up, all extensions can install' { | ||
write_allow_list '' | ||
rdctl extension install rd/extension/basic | ||
rdctl extension uninstall rd/extension/basic | ||
} | ||
|
||
@test 'when an extension is explicitly allowed, it can be installed' { | ||
write_allow_list '["rd/extension/basic:latest"]' | ||
rdctl extension install rd/extension/basic:latest | ||
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. I would also run |
||
rdctl extension uninstall rd/extension/basic | ||
} | ||
|
||
@test 'when an extension is not in the allowe list, it cannot be installed' { | ||
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. typo: "allowed" |
||
write_allow_list '["rd/extension/other"]' | ||
run rdctl extension install rd/extension/basic | ||
assert_failure | ||
} | ||
|
||
@test 'when no tags given, any tag is allowed' { | ||
write_allow_list '["rd/extension/basic"]' | ||
ctrctl "${namespace_arg[@]}" tag rd/extension/basic rd/extension/basic:0.0.3 | ||
rdctl extension install rd/extension/basic:0.0.3 | ||
rdctl extension uninstall rd/extension/basic | ||
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. Again, I would verify that the extension is actually installed, not just that |
||
} | ||
|
||
@test 'when tags are given, only the specified tag is allowed' { | ||
sleep 20 | ||
write_allow_list '["rd/extension/basic:0.0.2"]' | ||
ctrctl "${namespace_arg[@]}" tag rd/extension/basic rd/extension/basic:0.0.3 | ||
run rdctl extension install rd/extension/basic:0.0.3 | ||
assert_failure | ||
} | ||
|
||
@test 'extensions can be allowed by organization' { | ||
write_allow_list '["rd/extension/"]' | ||
rdctl extension install rd/extension/basic | ||
rdctl extension uninstall rd/extension/basic | ||
} | ||
|
||
@test 'extensions can be allowed by repository host' { | ||
write_allow_list '["registry.test/"]' | ||
ctrctl "${namespace_arg[@]}" tag rd/extension/basic registry.test/basic:0.0.3 | ||
rdctl extension install registry.test/basic:0.0.3 | ||
rdctl extension uninstall registry.test/basic | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { ExtensionImpl } from '@pkg/main/extensions/extensions'; | ||
|
||
describe('ExtensionImpl', () => { | ||
describe('checkInstallAllowed', () => { | ||
const subject = ExtensionImpl['checkInstallAllowed']; | ||
|
||
it('should reject invalid image references', () => { | ||
expect(() => subject(undefined, '/')).toThrow(); | ||
}); | ||
|
||
it('should allow images if the allow list is not enabled', () => { | ||
expect(() => subject(undefined, 'image')).not.toThrow(); | ||
}); | ||
|
||
it('should disallow any images given an empty list', () => { | ||
expect(() => subject([], 'image')).toThrow(); | ||
}); | ||
|
||
it('should allow specified image', () => { | ||
expect(() => subject(['other', 'image'], 'image')).not.toThrow(); | ||
}); | ||
|
||
it('should reject unknown image', () => { | ||
expect(() => subject(['allowed'], 'image')).toThrow(); | ||
}); | ||
|
||
it('should support missing tags', () => { | ||
expect(() => subject(['image'], 'image:1.0.0')).not.toThrow(); | ||
}); | ||
|
||
it('should reject images with the wrong tag', () => { | ||
expect(() => subject(['image:0.1'], 'image:0.2')).toThrow(); | ||
}); | ||
|
||
it('should support image references with registries', () => { | ||
const ref = 'r.example.test:1234/org/name:tag'; | ||
|
||
expect(() => subject([ref], ref)).not.toThrow(); | ||
}); | ||
|
||
it('should support org-level references', () => { | ||
expect(() => subject(['test.invalid/org/'], 'test.invalid/org/image:tag')).not.toThrow(); | ||
}); | ||
|
||
it('should support registry-level references', () => { | ||
expect(() => subject(['registry.test/'], 'registry.test/image:tag')).not.toThrow(); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ import { | |
|
||
import type { ContainerEngineClient } from '@pkg/backend/containerClient'; | ||
import mainEvents from '@pkg/main/mainEvents'; | ||
import { parseImageReference } from '@pkg/utils/dockerUtils'; | ||
import Logging from '@pkg/utils/logging'; | ||
import paths from '@pkg/utils/paths'; | ||
import { defined } from '@pkg/utils/typeUtils'; | ||
|
@@ -142,9 +143,59 @@ export class ExtensionImpl implements Extension { | |
return this._iconName as Promise<string>; | ||
} | ||
|
||
async install(): Promise<boolean> { | ||
/** | ||
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. Uncaught typo from an earlier PR: |
||
* Check if the given image is allowed to be installed according to the | ||
* extension allow list. | ||
* @throws If the image is not allowed to be installed. | ||
*/ | ||
protected static checkInstallAllowed(allowedImages: readonly string[] | undefined, image: string) { | ||
const desired = parseImageReference(image); | ||
const code = ExtensionErrorCode.INSTALL_DENIED; | ||
const prefix = `Disallowing install of ${ image }:`; | ||
|
||
if (!desired) { | ||
throw new ExtensionErrorImpl(code, `${ prefix } Invalid image reference`); | ||
} | ||
if (!allowedImages) { | ||
return; | ||
} | ||
for (const pattern of allowedImages) { | ||
const allowed = parseImageReference(pattern, true); | ||
|
||
if (allowed?.tag && allowed.tag !== desired.tag) { | ||
// This pattern doesn't match the tag, look for something else. | ||
continue; | ||
} | ||
|
||
if (allowed?.registry.href !== desired.registry.href) { | ||
// This pattern has a different registry | ||
continue; | ||
} | ||
|
||
if (!allowed.name) { | ||
// If there's no name given, the whole registry is allowed. | ||
return ''; | ||
} | ||
|
||
if (allowed.name.endsWith('/')) { | ||
if (desired.name.startsWith(allowed.name)) { | ||
// The allowed pattern ends with a slash, anything in the org is fine. | ||
return ''; | ||
} | ||
} else if (allowed.name === desired.name) { | ||
return ''; | ||
} | ||
} | ||
|
||
throw new ExtensionErrorImpl(code, `${ prefix } Image is not allowed`); | ||
} | ||
|
||
async install(allowedImages: readonly string[] | undefined): Promise<boolean> { | ||
const metadata = await this.metadata; | ||
|
||
ExtensionImpl.checkInstallAllowed(allowedImages, this.image); | ||
console.debug(`Image ${ this.image } is allowed to install: ${ allowedImages }`); | ||
|
||
await fs.promises.mkdir(this.dir, { recursive: true }); | ||
try { | ||
await this.installMetadata(this.dir, metadata); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -211,6 +211,8 @@ class ExtensionManagerImpl implements ExtensionManager { | |
|
||
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. Another question about an older commit: why is the block through which 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.
|
||
// Install / uninstall extensions as needed. | ||
const tasks: Promise<any>[] = []; | ||
const { enabled: allowEnabled, list: allowListRaw } = config.application.extensions.allowed; | ||
const allowList = allowEnabled ? allowListRaw : undefined; | ||
|
||
for (const [repo, tag] of Object.entries(config.extensions)) { | ||
if (!tag) { | ||
|
@@ -221,7 +223,7 @@ class ExtensionManagerImpl implements ExtensionManager { | |
|
||
tasks.push((async(id: string) => { | ||
try { | ||
return (await this.getExtension(id)).install(); | ||
return (await this.getExtension(id)).install(allowList); | ||
} catch (ex) { | ||
console.error(`Failed to install extension ${ id }`, ex); | ||
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. Another earlier-commit comment: FWICT,
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. Ah, yeah, that works. (Ideally we could use |
||
} | ||
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. Why is
Instead the code is like this:
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. Good catch; that is a bug. |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,10 +71,13 @@ export interface Extension { | |
|
||
/** | ||
* Install this extension. | ||
* @param allowedImages The list of extension images that are allowed to be | ||
* used; if all images are allowed, pass in undefined. | ||
* @note If the extension is already installed, this is a no-op. | ||
* @throws If the settings specify an allow list and this is not in it. | ||
* @return Whether the extension was installed. | ||
*/ | ||
install(): Promise<boolean>; | ||
install(allowedImages: readonly string[] | undefined): Promise<boolean>; | ||
/** | ||
* Uninstall this extension. | ||
* @note If the extension was not installed, this is a no-op. | ||
|
@@ -169,6 +172,7 @@ export const ExtensionErrorMarker = Symbol('extension-error'); | |
export enum ExtensionErrorCode { | ||
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. From a recent commit, 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. Thanks! |
||
INVALID_METADATA, | ||
FILE_NOT_FOUND, | ||
INSTALL_DENIED, | ||
} | ||
|
||
export interface ExtensionError extends Error { | ||
|
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.
Maybe a test that passes more than one arg to this function, because currently it only handles 0 or 1 values in the list?
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 takes a single argument that is the JSON encoded list.
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 realized that, but none of the args contain a comma
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.
Ah, I thought I had one. Will fix.