Skip to content

Commit

Permalink
fix searching on non-searchable image registries
Browse files Browse the repository at this point in the history
fixes: cockpit-project#1220

Add support to pull images from registries which do not support search
API.
  • Loading branch information
tomasmatus committed Sep 30, 2024
1 parent 5347a5f commit 5854327
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 11 deletions.
68 changes: 58 additions & 10 deletions src/ImageRunModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,24 @@ export class ImageRunModal extends React.Component {
});
};

buildFilterRegex(searchText) {
// Strip out all non-allowed container image characters when filtering.
let regexString = searchText.replace(/[^/\w_.:-]/g, "");
// Strip image registry option if set for comparing results for docker.io searching for docker.io/fedora
// returns docker.io/$username/fedora for example.
if (regexString.includes('/')) {
regexString = searchText.replace(searchText.split('/')[0], '');
}

return new RegExp(regexString, 'i');
}

onSearchTriggered = value => {
const imageExistsLocally = (searchText, localImages) => {
const regex = this.buildFilterRegex(searchText);
return localImages.some(localImage => localImage.Name.search(regex) !== -1);
};

// Do not call the SearchImage API if the input string is not at least 2 chars,
// The comparison was done considering the fact that we miss always one letter due to delayed setState
if (value.length < 2)
Expand Down Expand Up @@ -393,6 +410,15 @@ export class ImageRunModal extends React.Component {
})));
}

// Query if used url is a valid container that can be pulled
if ((value.includes('.') || value.startsWith("localhost")) && value.includes('/')) {
searches.push(this.activeConnection.call({
method: "GET",
path: client.VERSION + "libpod/manifests/" + value + "/json",
body: "",
}));
}

Promise.allSettled(searches)
.then(reply => {
if (reply && this._isMounted) {
Expand All @@ -402,17 +428,43 @@ export class ImageRunModal extends React.Component {

for (const result of reply) {
if (result.status === "fulfilled") {
imageResults = imageResults.concat(JSON.parse(result.value));
try {
imageResults = imageResults.concat(JSON.parse(result.value));
} catch (err) {
console.error(err);
}
} else {
// manifests query returns 404 Not Found in case used URL is not an image
// supress this error as it will either result in "No images found" or prioritize other erros from search queries
if (result.reason.status === 404) {
continue;
}
// Supress "could't search registry" error when local image is found
if (result.reason.message.includes("couldn't search registry") && imageExistsLocally(value, this.props.localImages)) {
continue;
}
dialogError = _("Failed to search for new images");
// TODO: add registry context, podman does not include it in the reply.
dialogErrorDetail = result.reason ? cockpit.format(_("Failed to search for images: $0"), result.reason.message) : _("Failed to search for images.");
}
}

// Group images on registry
const images = {};
imageResults.forEach(image => {
// Add Tag is it's there
// Manifests query
if (image.manifests !== undefined) {
// Only use manifests result if nothing else was found (non-searchable image repository)
if (imageResults.length === 1) {
image.Name = value;
dialogError = "";
dialogErrorDetail = "";
} else {
return;
}
}

// Add Tag if it's there
image.toString = function imageToString() {
if (this.Tag) {
return this.Name + ':' + this.Tag;
Expand All @@ -434,6 +486,7 @@ export class ImageRunModal extends React.Component {
images[index] = [image];
}
});

this.setState({
imageResults: images || {},
searchFinished: true,
Expand Down Expand Up @@ -484,6 +537,8 @@ export class ImageRunModal extends React.Component {
isImageSelectOpen: false,
command,
entrypoint,
dialogError: "",
dialogErrorDetail: "",
});
};

Expand Down Expand Up @@ -524,14 +579,7 @@ export class ImageRunModal extends React.Component {
imageRegistries.push(this.state.searchByRegistry);
}

// Strip out all non-allowed container image characters when filtering.
let regexString = searchText.replace(/[^/\w_.:-]/g, "");
// Strip image registry option if set for comparing results for docker.io searching for docker.io/fedora
// returns docker.io/$username/fedora for example.
if (regexString.includes('/')) {
regexString = searchText.replace(searchText.split('/')[0], '');
}
const input = new RegExp(regexString, 'i');
const input = this.buildFilterRegex(searchText);

const results = imageRegistries
.map((reg, index) => {
Expand Down
82 changes: 81 additions & 1 deletion test/check-application
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ REGISTRIES_CONF = """
registries = ['localhost:5000', 'localhost:6000']
[registries.insecure]
registries = ['localhost:5000', 'localhost:6000']
registries = ['localhost:80', 'localhost:5000', 'localhost:6000']
"""

NOT_RUNNING = ["Exited", "Stopped"]
Expand All @@ -28,6 +28,22 @@ IMG_BUSYBOX_LATEST = IMG_BUSYBOX + ":latest"
IMG_REGISTRY = "localhost/test-registry"
IMG_REGISTRY_LATEST = IMG_REGISTRY + ":latest"

NGINX_DEFAULT_CONF = """
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
proxy_pass http://localhost:5000/;
}
location ^~ /v2/_catalog {
return 403;
}
}
"""


def podman_version(cls):
version = cls.execute(False, "podman -v").strip().split(' ')[-1]
Expand Down Expand Up @@ -275,6 +291,11 @@ class TestApplication(testlib.MachineCase):
podman run -d -p 6000:5000 --name registry_alt --stop-timeout 0 {IMG_REGISTRY}
""")

self.addCleanup(m.execute, """
systemctl stop nginx.service
setsebool -P httpd_can_network_connect 0
rm -f /etc/nginx/default.conf
""")
# Add local insecure registry into registries conf
m.write("/etc/containers/registries.conf", REGISTRIES_CONF)
self.execute(True, "systemctl stop podman.service")
Expand Down Expand Up @@ -3049,6 +3070,65 @@ class TestApplication(testlib.MachineCase):
else:
self.assertNotIn("Podman", b.text("#host-apps"))

@testlib.skipImage("nginx not installed", "centos-*", "rhel-*", "debian-*", "ubuntu-*", "arch")
@testlib.skipOstree("nginx not installed")
def testNonSearchableRegistry(self):
b = self.browser
m = self.machine

self.setupRegistry()

# Nginx config simulates behavior of ghcr.io in terms of `podman search` and `podman manifest inspect`
self.execute(True, f"""
echo '{NGINX_DEFAULT_CONF}' > /etc/nginx/conf.d/default.conf
setsebool -P httpd_can_network_connect 1
systemctl start nginx.service
""")
self.addCleanup(m.execute, """
systemctl stop nginx.service
setsebool -P httpd_can_network_connect 0
rm -f /etc/nginx/default.conf
""")

container_name = "localhost:80/my-busybox"
m.execute(f"podman tag {IMG_BUSYBOX} {container_name}; podman push {container_name}")
# Untag busybox image which duplicates the image we are about to download
m.execute(f"podman rmi -f {IMG_BUSYBOX} localhost:80/my-busybox")

self.login()

b.click("#containers-containers button.pf-v5-c-button.pf-m-primary")
b.set_input_text("#run-image-dialog-name", "new-busybox")
# Searching for container by prefix fails
b.set_input_text("#create-image-image-select-typeahead", "localhost:80/my-busy")
b.wait_text("button.pf-v5-c-select__menu-item.pf-m-disabled", "No images found")
b.wait_in_text(".pf-v5-c-alert.pf-m-danger", "couldn't search registry")

# Giving full image name finds valid manifest therefore image is pullable
b.set_input_text("#create-image-image-select-typeahead", container_name)
b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", container_name)
# Select image and create a container
b.click(f"button.pf-v5-c-select__menu-item:contains({container_name})")
b.click("#create-image-create-btn")

# Wait for download to finish
sel = "span:not(.downloading)"
b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in "Created")

b.click("#containers-containers button.pf-v5-c-button.pf-m-primary")
# "couldn't search registry" error is hidden when some local image is found
b.set_input_text("#create-image-image-select-typeahead", "localhost:80/my-busy")
b.wait_not_present(".pf-v5-c-alert.pf-m-danger")
b.click('button.pf-v5-c-toggle-group__button:contains("Local")')
b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", f"{container_name}:latest")

# Error is shown again when no image was found locally
b.set_input_text("#create-image-image-select-typeahead", "localhost:80/their-busy")
b.wait_text("button.pf-v5-c-select__menu-item.pf-m-disabled", "No images found")
b.wait_in_text(".pf-v5-c-alert.pf-m-danger", "couldn't search registry")

testlib.sit()


if __name__ == '__main__':
testlib.test_main()

0 comments on commit 5854327

Please sign in to comment.