From 8ce44813f8fc6f338b4f4503a003f88160cb524e Mon Sep 17 00:00:00 2001 From: Jakub Hadvig Date: Fri, 15 May 2026 14:48:07 +0200 Subject: [PATCH] CONSOLE-5293: Use console-downloads image instead of cli-artifacts Replace the cli-artifacts image with the new console-downloads image for the downloads deployment. The new image contains a Golang HTTP server with its own entrypoint, so the inline Python server script and associated command/args/volumes are removed from the deployment template. --- bindata/assets/deployments/Untitled | 1 + .../deployments/downloads-deployment.yaml | 201 +----------------- manifests/07-operator-ibm-cloud-managed.yaml | 2 +- manifests/07-operator.yaml | 2 +- .../subresource/deployment/deployment_test.go | 26 +-- 5 files changed, 14 insertions(+), 218 deletions(-) create mode 100644 bindata/assets/deployments/Untitled diff --git a/bindata/assets/deployments/Untitled b/bindata/assets/deployments/Untitled new file mode 100644 index 0000000000..7d76ef0947 --- /dev/null +++ b/bindata/assets/deployments/Untitled @@ -0,0 +1 @@ +readOnlyRootFilesystem \ No newline at end of file diff --git a/bindata/assets/deployments/downloads-deployment.yaml b/bindata/assets/deployments/downloads-deployment.yaml index 5fd1a64dc4..9b64c431ca 100644 --- a/bindata/assets/deployments/downloads-deployment.yaml +++ b/bindata/assets/deployments/downloads-deployment.yaml @@ -48,17 +48,16 @@ spec: successThreshold: 1 failureThreshold: 3 name: download-server + command: + - /opt/downloads/downloads + args: + - --config-path=/opt/downloads/defaultArtifactsConfig.yaml securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: - ALL - volumeMounts: - - mountPath: /tmp - name: tmp - command: - - /bin/sh livenessProbe: httpGet: path: / @@ -75,198 +74,6 @@ spec: imagePullPolicy: IfNotPresent terminationMessagePolicy: FallbackToLogsOnError image: ${IMAGE} - args: - - '-c' - - | - cat </tmp/serve.py - import errno, http.server, os, re, signal, socket, sys, tarfile, tempfile, threading, time, zipfile - - def shutdown_handler(signum, frame): - print("Received signal {}, shutting down...".format(signum), flush=True) - os._exit(0) - signal.signal(signal.SIGTERM, shutdown_handler) - - def write_index(path, message): - with open(path, 'wb') as f: - f.write('\n'.join([ - '', - '', - '', - ' ', - '', - '', - ' {}'.format(message), - '', - '', - '', - ]).encode('utf-8')) - - # Launch multiple listeners as threads - class Thread(threading.Thread): - def __init__(self, i, socket): - threading.Thread.__init__(self) - self.i = i - self.socket = socket - self.daemon = True - self.start() - - def run(self): - server = http.server.SimpleHTTPRequestHandler - server.server_version = "OpenShift Downloads Server" - server.sys_version = "" - httpd = http.server.HTTPServer(addr, server, False) - - # Prevent the HTTP server from re-binding every handler. - # https://stackoverflow.com/questions/46210672/ - httpd.socket = self.socket - httpd.server_bind = self.server_close = lambda self: None - - httpd.serve_forever() - - print('Starting downloads server...', flush=True) - temp_dir = tempfile.mkdtemp() - print('Serving from: {}'.format(temp_dir), flush=True) - os.chdir(temp_dir) - - print('Creating arch directories...', flush=True) - for arch in ['amd64', 'arm64', 'ppc64le', 's390x']: - os.mkdir(arch) - - content = ['license'] - print('Creating license symlink...', flush=True) - os.symlink('/usr/share/openshift/LICENSE', 'oc-license') - - # Function to create archives in background - def create_archives_async(arch, operating_system, path, basename, archive_path_root): - try: - print(' [Background] Creating archives for {} {}...'.format(arch, operating_system), flush=True) - - print(' [Background] Creating tar archive...', flush=True) - with tarfile.open('{}.tar'.format(archive_path_root), 'w') as tar: - tar.add(path, basename) - - print(' [Background] Creating zip archive...', flush=True) - with zipfile.ZipFile('{}.zip'.format(archive_path_root), 'w') as zip: - zip.write(path, basename) - - print(' [Background] Done with archives for {} {}'.format(arch, operating_system), flush=True) - except Exception as e: - print(' [Background] ERROR creating archives for {} {}: {}'.format(arch, operating_system, str(e)), flush=True) - - print('Creating oc binary symlinks (archives will be created asynchronously)...', flush=True) - archive_threads = [] - - for arch, operating_system, path in [ - ('amd64', 'linux', '/usr/share/openshift/linux_amd64/oc'), - ('amd64', 'linux', '/usr/share/openshift/linux_amd64/oc.rhel8'), - ('amd64', 'linux', '/usr/share/openshift/linux_amd64/oc.rhel9'), - ('amd64', 'mac', '/usr/share/openshift/mac/oc'), - ('amd64', 'windows', '/usr/share/openshift/windows/oc.exe'), - ('arm64', 'linux', '/usr/share/openshift/linux_arm64/oc'), - ('arm64', 'linux', '/usr/share/openshift/linux_arm64/oc.rhel8'), - ('arm64', 'linux', '/usr/share/openshift/linux_arm64/oc.rhel9'), - ('arm64', 'mac', '/usr/share/openshift/mac_arm64/oc'), - ('ppc64le', 'linux', '/usr/share/openshift/linux_ppc64le/oc'), - ('ppc64le', 'linux', '/usr/share/openshift/linux_ppc64le/oc.rhel8'), - ('ppc64le', 'linux', '/usr/share/openshift/linux_ppc64le/oc.rhel9'), - ('s390x', 'linux', '/usr/share/openshift/linux_s390x/oc'), - ('s390x', 'linux', '/usr/share/openshift/linux_s390x/oc.rhel8'), - ('s390x', 'linux', '/usr/share/openshift/linux_s390x/oc.rhel9'), - ]: - try: - print(' Processing {} {} ({})...'.format(arch, operating_system, path), flush=True) - - # Check if source file exists - if not os.path.exists(path): - print(' WARNING: {} does not exist, skipping'.format(path), flush=True) - continue - - file_size = os.path.getsize(path) - print(' Source file size: {} MB'.format(file_size // (1024*1024)), flush=True) - - basename = os.path.basename(path) - target_path = os.path.join(arch, operating_system, basename) - - print(' Creating directory...', flush=True) - os.makedirs(os.path.join(arch, operating_system), exist_ok=True) - - print(' Creating symlink...', flush=True) - os.symlink(path, target_path) - - # Only strip .exe extension, keep everything else (e.g., oc.rhel8 stays oc.rhel8) - if basename.endswith('.exe'): - base_root = basename[:-4] - else: - base_root = basename - archive_path_root = os.path.join(arch, operating_system, base_root) - - # Start background thread to create archives - archive_thread = threading.Thread( - target=create_archives_async, - args=(arch, operating_system, path, basename, archive_path_root), - daemon=True - ) - archive_thread.start() - archive_threads.append(archive_thread) - - content.append( - 'oc ({1} {2}) (tar zip)'.format( - target_path, arch, operating_system, archive_path_root - ) - ) - print(' Done with {} {} (archives creating in background)'.format(arch, operating_system), flush=True) - except Exception as e: - print(' ERROR processing {} {}: {}'.format(arch, operating_system, str(e)), flush=True) - - print('All symlinks created. {} background threads creating archives...'.format(len(archive_threads)), flush=True) - - for root, directories, filenames in os.walk(temp_dir): - root_link = os.path.relpath(temp_dir, os.path.join(root, 'child')).replace(os.path.sep, '/') - for directory in directories: - write_index( - path=os.path.join(root, directory, 'index.html'), - message='

Directory listings are disabled. See here for available content.

'.format(root_link), - ) - - write_index( - path=os.path.join(temp_dir, 'index.html'), - message='\n'.join( - ['

Note: Archive files (.tar, .zip) are generated on server startup and may take a few moments to become available.

'] + - [''] - ), - ) - - # Create socket - # IPv6 should handle IPv4 passively so long as it is not bound to a - # specific address or set to IPv6_ONLY - # https://stackoverflow.com/questions/25817848/python-3-does-http-server-support-ipv6 - try: - addr = ('::', 8080) - sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - except socket.error as err: - # errno.EAFNOSUPPORT is "socket.error: [Errno 97] Address family not supported by protocol" - # When IPv6 is disabled, socket will bind using IPv4. - if err.errno == errno.EAFNOSUPPORT: - addr = ('', 8080) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - else: - raise - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - print('Binding to {}...'.format(addr), flush=True) - sock.bind(addr) - sock.listen(5) - - print('Starting 100 worker threads...', flush=True) - [Thread(i, socket=sock) for i in range(100)] - print('Server ready on port 8080!', flush=True) - time.sleep(9e9) - EOF - exec python3 /tmp/serve.py - volumes: - - name: tmp - emptyDir: {} tolerations: - key: node-role.kubernetes.io/master operator: Exists diff --git a/manifests/07-operator-ibm-cloud-managed.yaml b/manifests/07-operator-ibm-cloud-managed.yaml index eabfb01e16..d28831a346 100644 --- a/manifests/07-operator-ibm-cloud-managed.yaml +++ b/manifests/07-operator-ibm-cloud-managed.yaml @@ -32,7 +32,7 @@ spec: - name: CONSOLE_IMAGE value: registry.svc.ci.openshift.org/openshift:console - name: DOWNLOADS_IMAGE - value: registry.svc.ci.openshift.org/openshift:cli-artifacts + value: registry.svc.ci.openshift.org/openshift:console-downloads - name: OPERATOR_IMAGE_VERSION value: 0.0.1-snapshot - name: OPERATOR_NAME diff --git a/manifests/07-operator.yaml b/manifests/07-operator.yaml index 82d03645c2..5d802cc93d 100644 --- a/manifests/07-operator.yaml +++ b/manifests/07-operator.yaml @@ -70,7 +70,7 @@ spec: - name: CONSOLE_IMAGE value: registry.svc.ci.openshift.org/openshift:console - name: DOWNLOADS_IMAGE - value: registry.svc.ci.openshift.org/openshift:cli-artifacts + value: registry.svc.ci.openshift.org/openshift:console-downloads - name: OPERATOR_IMAGE_VERSION value: "0.0.1-snapshot" - name: OPERATOR_NAME diff --git a/pkg/console/subresource/deployment/deployment_test.go b/pkg/console/subresource/deployment/deployment_test.go index e305acf697..35ccc832ee 100644 --- a/pkg/console/subresource/deployment/deployment_test.go +++ b/pkg/console/subresource/deployment/deployment_test.go @@ -1591,12 +1591,11 @@ func TestWithConsoleNodeSelector(t *testing.T) { func TestDefaultDownloadsDeployment(t *testing.T) { var ( - defaultReplicaCount int32 = DefaultConsoleReplicas - singleNodeReplicaCount int32 = SingleNodeConsoleReplicas - labels = util.LabelsForDownloads() - gracePeriod int64 = 5 - tolerationSeconds int64 = 120 - downloadsDeploymentTemplate = resourceread.ReadDeploymentV1OrDie(bindata.MustAsset("assets/deployments/downloads-deployment.yaml")) + defaultReplicaCount int32 = DefaultConsoleReplicas + singleNodeReplicaCount int32 = SingleNodeConsoleReplicas + labels = util.LabelsForDownloads() + gracePeriod int64 = 5 + tolerationSeconds int64 = 120 ) type args struct { @@ -1680,11 +1679,6 @@ func TestDefaultDownloadsDeployment(t *testing.T) { Protocol: corev1.ProtocolTCP, ContainerPort: api.DownloadsPort, }}, - VolumeMounts: []corev1.VolumeMount{{ - Name: "tmp", - ReadOnly: false, - MountPath: "/tmp", - }}, ReadinessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ @@ -1711,14 +1705,14 @@ func TestDefaultDownloadsDeployment(t *testing.T) { SuccessThreshold: 1, FailureThreshold: 3, }, - Command: []string{"/bin/sh"}, + Command: []string{"/opt/downloads/downloads"}, Resources: corev1.ResourceRequirements{ Requests: map[corev1.ResourceName]resource.Quantity{ corev1.ResourceCPU: resource.MustParse("10m"), corev1.ResourceMemory: resource.MustParse("50Mi"), }, }, - Args: downloadsDeploymentTemplate.Spec.Template.Spec.Containers[0].Args, + Args: []string{"--config-path=/opt/downloads/defaultArtifactsConfig.yaml"}, SecurityContext: &corev1.SecurityContext{ ReadOnlyRootFilesystem: utilpointer.Bool(true), Capabilities: &corev1.Capabilities{ @@ -1730,12 +1724,6 @@ func TestDefaultDownloadsDeployment(t *testing.T) { }, }, }, - Volumes: []corev1.Volume{{ - Name: "tmp", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }}, } downloadsDeploymentPodSpecHighAvail := downloadsDeploymentPodSpecSingleReplica.DeepCopy() downloadsDeploymentPodSpecHighAvail.Affinity = &corev1.Affinity{