diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 94bb8db53..85f50fad1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -74,6 +74,63 @@ jobs:
run: npm ci
- name: Attempt a build
run: npm run build:ocp
+ build-containers:
+ name: Build containers
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ os: [el9, el10]
+ variant: [standalone, ocp]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22.x
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Determine Containerfile
+ id: containerfile
+ run: |
+ if [[ "${{ matrix.variant }}" == "standalone" ]]; then
+ echo "file=packaging/images/${{ matrix.os }}/Containerfile" >> $GITHUB_OUTPUT
+ else
+ echo "file=packaging/images/${{ matrix.os }}/Containerfile.ocp" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Build container (${{ matrix.os }} ${{ matrix.variant }})
+ run: |
+ echo "Building ${{ matrix.variant }} UI container for ${{ matrix.os }}..."
+ podman build -f ${{ steps.containerfile.outputs.file }} -t test-ui:${{ matrix.os }}-${{ matrix.variant }} .
+ echo "✓ Successfully built ${{ matrix.os }} ${{ matrix.variant }} container"
+
+ - name: Verify container - help command
+ id: verify-help
+ run: |
+ echo "Testing help command..."
+ if timeout 30s podman run --rm test-ui:${{ matrix.os }}-${{ matrix.variant }} ./flightctl-ui --help; then
+ echo "Help command successful"
+ echo "passed=true" >> $GITHUB_OUTPUT
+ else
+ echo "Help command failed"
+ echo "passed=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Verify container - binary exists
+ if: steps.verify-help.outputs.passed != 'true'
+ run: |
+ echo "Help command failed, checking if binary exists..."
+ if timeout 30s podman run --rm test-ui:${{ matrix.os }}-${{ matrix.variant }} ls -la /app/proxy/flightctl-ui; then
+ echo "Binary exists but help command failed"
+ else
+ echo "Binary missing"
+ exit 1
+ fi
+
integration-tests:
needs: preflight-check
if: needs.preflight-check.outputs.skip == 'false'
diff --git a/.github/workflows/push-to-main.yaml b/.github/workflows/push-to-main.yaml
index 5829b35ff..715940fde 100644
--- a/.github/workflows/push-to-main.yaml
+++ b/.github/workflows/push-to-main.yaml
@@ -46,14 +46,23 @@ jobs:
publish-flightctl-ui:
runs-on: ubuntu-latest
needs: [generate-tags]
+ strategy:
+ matrix:
+ os: [el9, el10]
steps:
- uses: actions/checkout@v4
+ - name: Generate OS-specific image name
+ id: os-image
+ run: |
+ echo "image_name=${{ env.QUAY_STANDALONE_REPO }}-${{ matrix.os }}" >> $GITHUB_OUTPUT
+ echo "Generated image name: ${{ env.QUAY_STANDALONE_REPO }}-${{ matrix.os }}"
+
- name: Build
id: build
uses: redhat-actions/buildah-build@v2
with:
- image: ${{ env.QUAY_STANDALONE_REPO }}
+ image: ${{ steps.os-image.outputs.image_name }}
tags: ${{ needs.generate-tags.outputs.image_tags }}
labels: |
org.flightctl.flightctl-ui.github.repository=${{ github.repository }}
@@ -63,7 +72,7 @@ jobs:
org.flightctl.flightctl-ui.github.ref_name=${{ github.ref_name }}
extra-args: |
--ulimit nofile=10000:10000
- containerfiles: Containerfile
+ containerfiles: packaging/images/${{ matrix.os }}/Containerfile
context: .
- name: Validate FIPS
@@ -86,14 +95,23 @@ jobs:
publish-flightctl-ocp-ui:
runs-on: ubuntu-latest
needs: [generate-tags]
+ strategy:
+ matrix:
+ os: [el9, el10]
steps:
- uses: actions/checkout@v4
+ - name: Generate OS-specific image name
+ id: os-image
+ run: |
+ echo "image_name=${{ env.QUAY_OCP_REPO }}-${{ matrix.os }}" >> $GITHUB_OUTPUT
+ echo "Generated image name: ${{ env.QUAY_OCP_REPO }}-${{ matrix.os }}"
+
- name: Build
id: build
uses: redhat-actions/buildah-build@v2
with:
- image: ${{ env.QUAY_OCP_REPO }}
+ image: ${{ steps.os-image.outputs.image_name }}
tags: ${{ needs.generate-tags.outputs.image_tags }}
labels: |
org.flightctl.flightctl-ui.github.repository=${{ github.repository }}
@@ -103,7 +121,7 @@ jobs:
org.flightctl.flightctl-ui.github.ref_name=${{ github.ref_name }}
extra-args: |
--ulimit nofile=10000:10000
- containerfiles: Containerfile.ocp
+ containerfiles: packaging/images/${{ matrix.os }}/Containerfile.ocp
context: .
- name: Validate FIPS
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..2c156db7e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,84 @@
+# FlightCtl UI Makefile for EL9/EL10 parallel support
+
+# Default OS - can be overridden with OS=el10
+OS ?= el9
+
+# Container registry settings
+REGISTRY ?= quay.io
+REGISTRY_OWNER ?= flightctl
+
+# Version/tag settings
+SOURCE_GIT_TAG ?= $(shell git describe --long --tags --exclude latest --dirty)
+VERSION ?= $(shell echo $(SOURCE_GIT_TAG) | sed 's/^v//')
+
+# Image names
+STANDALONE_IMAGE_NAME = flightctl-ui
+OCP_IMAGE_NAME = flightctl-ocp-ui
+
+# Build targets
+.PHONY: help build-ui build-ocp-ui build-all clean
+
+help: ## Show this help message
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+build-ui: packaging/images/$(OS)/Containerfile ## Build standalone UI container for current OS (default: el9)
+ @echo "Building standalone UI container for $(OS)..."
+ podman build \
+ -f packaging/images/$(OS)/Containerfile \
+ -t localhost/$(STANDALONE_IMAGE_NAME)-$(OS):latest \
+ -t $(REGISTRY)/$(REGISTRY_OWNER)/$(STANDALONE_IMAGE_NAME)-$(OS):$(VERSION) \
+ .
+
+build-ocp-ui: packaging/images/$(OS)/Containerfile.ocp ## Build OCP UI container for current OS (default: el9)
+ @echo "Building OCP UI container for $(OS)..."
+ podman build \
+ -f packaging/images/$(OS)/Containerfile.ocp \
+ -t localhost/$(OCP_IMAGE_NAME)-$(OS):latest \
+ -t $(REGISTRY)/$(REGISTRY_OWNER)/$(OCP_IMAGE_NAME)-$(OS):$(VERSION) \
+ .
+
+build-ui-el9: ## Build standalone UI container for EL9
+ $(MAKE) build-ui OS=el9
+
+build-ui-el10: ## Build standalone UI container for EL10
+ $(MAKE) build-ui OS=el10
+
+build-ocp-ui-el9: ## Build OCP UI container for EL9
+ $(MAKE) build-ocp-ui OS=el9
+
+build-ocp-ui-el10: ## Build OCP UI container for EL10
+ $(MAKE) build-ocp-ui OS=el10
+
+build-all: build-ui-el9 build-ui-el10 build-ocp-ui-el9 build-ocp-ui-el10 ## Build all UI containers (both OS variants)
+
+clean: ## Clean up built containers
+ @echo "Cleaning up UI containers..."
+ podman rmi -f localhost/$(STANDALONE_IMAGE_NAME)-el9:latest localhost/$(STANDALONE_IMAGE_NAME)-el10:latest 2>/dev/null || true
+ podman rmi -f localhost/$(OCP_IMAGE_NAME)-el9:latest localhost/$(OCP_IMAGE_NAME)-el10:latest 2>/dev/null || true
+ @echo "Cleanup complete"
+
+# Development targets
+dev: ## Run standalone UI in development mode
+ npm run dev
+
+dev-ocp: ## Run OCP plugin UI in development mode
+ npm run dev:ocp
+
+install: ## Install npm dependencies
+ npm ci
+
+build: ## Build UI applications (npm build)
+ npm run build
+
+build-ocp: ## Build OCP plugin application
+ npm run build:ocp
+
+lint: ## Run linting
+ npm run lint
+
+# Show current configuration
+show-config: ## Show current build configuration
+ @echo "OS: $(OS)"
+ @echo "VERSION: $(VERSION)"
+ @echo "REGISTRY: $(REGISTRY)"
+ @echo "REGISTRY_OWNER: $(REGISTRY_OWNER)"
\ No newline at end of file
diff --git a/README.md b/README.md
index de16ea5b8..bd11b1f32 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,9 @@ Monorepo containing UIs for [Flight Control](https://github.com/flightctl/flight
## Building
-### Checkout the repository and run
+### JavaScript/TypeScript Applications
+
+Checkout the repository and run:
```shell
cd flightctl-ui
@@ -16,6 +18,37 @@ npm ci
npm run build
```
+### Container Images (EDM-3308: EL9/EL10 Support)
+
+The UI supports building containers for both Enterprise Linux 9 and 10. Containerfiles are organized by OS in a directory structure similar to the main FlightCtl repository:
+
+```text
+packaging/images/el9/Containerfile # EL9 standalone UI
+packaging/images/el9/Containerfile.ocp # EL9 OCP plugin UI
+packaging/images/el10/Containerfile # EL10 standalone UI
+packaging/images/el10/Containerfile.ocp # EL10 OCP plugin UI
+```
+
+Use the provided Makefile:
+
+```shell
+# Build for specific OS (default: el9)
+make build-ui OS=el9 # Standalone UI for EL9
+make build-ui OS=el10 # Standalone UI for EL10
+make build-ocp-ui OS=el9 # OCP Plugin UI for EL9
+make build-ocp-ui OS=el10 # OCP Plugin UI for EL10
+
+# Build all variants
+make build-all
+
+# Show available targets
+make help
+```
+
+Built images will use OS-qualified names:
+- `localhost/flightctl-ui-el9:latest` and `localhost/flightctl-ocp-ui-el9:latest`
+- `localhost/flightctl-ui-el10:latest` and `localhost/flightctl-ocp-ui-el10:latest`
+
### Running Standalone UI with backend running in Kind
If backend is running in your Kind cluster, use the following command to start the UI application.
diff --git a/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx b/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx
index 74256472d..9343df6e8 100644
--- a/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx
+++ b/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx
@@ -34,6 +34,9 @@ import FlightCtlWizardFooter from '../../common/FlightCtlWizardFooter';
import LeaveFormConfirmation from '../../common/LeaveFormConfirmation';
import ErrorBoundary from '../../common/ErrorBoundary';
import { getErrorMessage } from '../../../utils/error';
+import { usePermissionsContext } from '../../common/PermissionsContext';
+import PageWithPermissions from '../../common/PageWithPermissions';
+import { RESOURCE, VERB } from '../../../types/rbac';
const orderedIds = [generalInfoStepId, typeConfigStepId, versionStepId, reviewStepId];
@@ -250,4 +253,26 @@ const AddCatalogItemWizard = () => {
);
};
-export default AddCatalogItemWizard;
+const addCatalogItemWizardPermissions = [
+ { kind: RESOURCE.CATALOG, verb: VERB.LIST },
+ { kind: RESOURCE.CATALOG_ITEM, verb: VERB.GET },
+ { kind: RESOURCE.CATALOG_ITEM, verb: VERB.CREATE },
+ { kind: RESOURCE.CATALOG_ITEM, verb: VERB.PATCH },
+];
+
+const AddCatalogItemWizardWithPermissions = () => {
+ const {
+ router: { useParams },
+ } = useAppContext();
+ const { catalogId, itemId } = useParams<{ catalogId: string; itemId: string }>();
+ const isEdit = !!catalogId && !!itemId;
+ const { checkPermissions, loading } = usePermissionsContext();
+ const [canListCatalogs, canGet, canCreate, canPatch] = checkPermissions(addCatalogItemWizardPermissions);
+ return (
+
+
+
+ );
+};
+
+export default AddCatalogItemWizardWithPermissions;
diff --git a/libs/ui-components/src/components/Catalog/CatalogPage.tsx b/libs/ui-components/src/components/Catalog/CatalogPage.tsx
index d04977201..3a794d32a 100644
--- a/libs/ui-components/src/components/Catalog/CatalogPage.tsx
+++ b/libs/ui-components/src/components/Catalog/CatalogPage.tsx
@@ -43,6 +43,7 @@ import CreateCatalogModal from './AddCatalogItemWizard/CreateCatalogModal';
import WithTooltip from '../common/WithTooltip';
import ResourceSyncImportStatus from '../ResourceSync/ResourceSyncImportStatus';
import CatalogLandingPage, { CatalogLandingPageContent, useLandingPagePermissions } from './CatalogLandingPage';
+import PageWithPermissions from '../common/PageWithPermissions';
import './CatalogPage.css';
@@ -424,16 +425,19 @@ const catalogPagePermissions = [
{ kind: RESOURCE.DEVICE, verb: VERB.PATCH },
{ kind: RESOURCE.CATALOG, verb: VERB.PATCH },
{ kind: RESOURCE.CATALOG, verb: VERB.DELETE },
+ { kind: RESOURCE.CATALOG_ITEM, verb: VERB.LIST },
+ { kind: RESOURCE.CATALOG, verb: VERB.LIST },
];
const CatalogPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
- const { checkPermissions } = usePermissionsContext();
- const [canEditFleet, canEditDevice, canEditCatalog, canDeleteCatalog] = checkPermissions(catalogPagePermissions);
+ const { checkPermissions, loading } = usePermissionsContext();
+ const [canEditFleet, canEditDevice, canEditCatalog, canDeleteCatalog, canListItems, canListCatalogs] =
+ checkPermissions(catalogPagePermissions);
return (
- <>
+
{
showCatalogMgmt
/>
- >
+
);
};
diff --git a/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx b/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx
index f3d7d6cf9..b686f821b 100644
--- a/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx
+++ b/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx
@@ -30,6 +30,9 @@ import { useTranslation } from '../../../hooks/useTranslation';
import { useCatalogItem } from '../useCatalogs';
import { useFetchPeriodically } from '../../../hooks/useFetchPeriodically';
import { UpdateSuccessPageContent } from '../InstallWizard/UpdateSuccessPage';
+import { usePermissionsContext } from '../../common/PermissionsContext';
+import PageWithPermissions from '../../common/PageWithPermissions';
+import { RESOURCE, VERB } from '../../../types/rbac';
type EditWizardProps = {
specPath: string;
@@ -239,27 +242,33 @@ const EditWizard = ({
);
};
+const editWizardPermissions = [{ kind: RESOURCE.CATALOG_ITEM, verb: VERB.GET }];
+
export const EditDeviceWizard = () => {
const {
router: { useParams },
} = useAppContext();
const { deviceId } = useParams() as { deviceId: string };
+ const { checkPermissions, loading: permissionsLoading } = usePermissionsContext();
+ const [canGetItem] = checkPermissions(editWizardPermissions);
const [device, loading, error] = useFetchPeriodically>({
endpoint: `devices/${deviceId}`,
});
return (
-
+
+
+
);
};
@@ -268,20 +277,24 @@ export const EditFleetWizard = () => {
router: { useParams },
} = useAppContext();
const params = useParams() as { fleetId: string };
+ const { checkPermissions, loading: permissionsLoading } = usePermissionsContext();
+ const [canGetItem] = checkPermissions(editWizardPermissions);
const [fleet, loading, error] = useFetchPeriodically>({
endpoint: `fleets/${params.fleetId}`,
});
return (
-
+
+
+
);
};
diff --git a/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx b/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx
index abc36b139..8d029f38b 100644
--- a/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx
+++ b/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx
@@ -22,6 +22,9 @@ import InstallAppWizard from './InstallAppWizard';
import { useAppContext } from '../../../hooks/useAppContext';
import { useCatalogItem } from '../useCatalogs';
import { getErrorMessage } from '../../../utils/error';
+import { usePermissionsContext } from '../../common/PermissionsContext';
+import PageWithPermissions from '../../common/PageWithPermissions';
+import { RESOURCE, VERB } from '../../../types/rbac';
const InstallWizard = () => {
const { t } = useTranslation();
@@ -77,4 +80,16 @@ const InstallWizard = () => {
);
};
-export default InstallWizard;
+const installWizardPermissions = [{ kind: RESOURCE.CATALOG_ITEM, verb: VERB.GET }];
+
+const InstallWizardWithPermissions = () => {
+ const { checkPermissions, loading } = usePermissionsContext();
+ const [canGetItem] = checkPermissions(installWizardPermissions);
+ return (
+
+
+
+ );
+};
+
+export default InstallWizardWithPermissions;
diff --git a/libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx b/libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx
index 10c7c9b40..0689bfd03 100644
--- a/libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx
+++ b/libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx
@@ -6,6 +6,9 @@ import { CatalogItem } from '@flightctl/types/alpha';
import { getRemoveAppPatches, getRemoveOsPatches } from '../../Catalog/utils';
import { CatalogPageContent } from '../../Catalog/CatalogPage';
import InstalledSoftware from '../../Catalog/InstalledSoftware';
+import { usePermissionsContext } from '../../common/PermissionsContext';
+import { RESOURCE, VERB } from '../../../types/rbac';
+import PageWithPermissions from '../../common/PageWithPermissions';
import './ResourceCatalogPage.css';
@@ -20,6 +23,11 @@ type ResourceCatalogPageProps = {
onInstall: (installItem: { item: CatalogItem; channel: string; version: string }) => void;
};
+const catalogPagePermissions = [
+ { kind: RESOURCE.CATALOG_ITEM, verb: VERB.LIST },
+ { kind: RESOURCE.CATALOG, verb: VERB.LIST },
+];
+
const ResourceCatalogPage = ({
currentLabels,
spec,
@@ -30,6 +38,8 @@ const ResourceCatalogPage = ({
onEdit,
onInstall,
}: ResourceCatalogPageProps) => {
+ const { checkPermissions, loading } = usePermissionsContext();
+ const [canListItems, canListCatalogs] = checkPermissions(catalogPagePermissions);
const onDeleteOs = async () => {
const allPatches = getRemoveOsPatches({ currentLabels, specPath });
await onPatch(allPatches);
@@ -46,7 +56,7 @@ const ResourceCatalogPage = ({
};
return (
- <>
+
- >
+
);
};
diff --git a/libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx b/libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx
index 175aa0f26..a7e0931c6 100644
--- a/libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx
+++ b/libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx
@@ -35,6 +35,8 @@ import CatalogItemCard from '../Catalog/CatalogItemCard';
import { getFullContainerURI } from '../Catalog/utils';
import { DynamicFormContext } from './DynamicForm';
import { useTranslation } from '../../hooks/useTranslation';
+import { usePermissionsContext } from '../common/PermissionsContext';
+import { RESOURCE, VERB } from '../../types/rbac';
import TableTextSearch from '../Table/TableTextSearch';
import TablePagination from '../Table/TablePagination';
import {
@@ -262,6 +264,8 @@ const CatalogItemDetails = ({
);
};
+const catalogItemListPermission = [{ kind: RESOURCE.CATALOG_ITEM, verb: VERB.LIST }];
+
/**
* Custom field for the volume image "reference" property.
* Renders a text input plus "Choose from catalog" for Asset selection.
@@ -278,6 +282,8 @@ const VolumeImageField: React.FC = ({
readonly,
}) => {
const { t } = useTranslation();
+ const { checkPermissions } = usePermissionsContext();
+ const [canListCatalogItems] = checkPermissions(catalogItemListPermission);
const { onAssetSelected, selectedAssets, onAssetCleared } = formContext as DynamicFormContext;
const referenceValue = typeof formData === 'string' ? formData : '';
const volumeIndex = getVolumeIndexFromId(idSchema.$id);
@@ -343,11 +349,13 @@ const VolumeImageField: React.FC = ({
placeholder={t('Enter image reference or choose from catalog')}
/>
-
-
-
+ {canListCatalogItems && (
+
+
+
+ )}
)}
{schema.description && (
diff --git a/packaging/images/el10/Containerfile b/packaging/images/el10/Containerfile
new file mode 100644
index 000000000..01347db43
--- /dev/null
+++ b/packaging/images/el10/Containerfile
@@ -0,0 +1,33 @@
+FROM registry.access.redhat.com/ubi10/nodejs-22-minimal:latest as ui-build
+USER root
+RUN microdnf install -y rsync
+
+WORKDIR /app
+COPY package.json /app
+COPY package-lock.json /app
+COPY tsconfig.json /app
+COPY libs /app/libs
+COPY apps /app/apps
+ENV NODE_OPTIONS='--max-old-space-size=8192'
+RUN npm ci
+RUN npm run build
+
+FROM registry.access.redhat.com/ubi10/go-toolset:1.24.6-1763548447 as proxy-build
+WORKDIR /app
+COPY proxy /app
+USER 0
+RUN CGO_ENABLED=1 CGO_CFLAGS=-flto GOEXPERIMENT=strictfipsruntime go build
+
+FROM quay.io/flightctl/flightctl-base:el10-10.1-1769518576
+COPY --from=ui-build /app/apps/standalone/dist /app/proxy/dist
+COPY --from=proxy-build /app/flightctl-ui /app/proxy
+WORKDIR /app/proxy
+LABEL \
+ com.redhat.component="flightctl-ui-container" \
+ description="Flight Control User Interface Service" \
+ io.k8s.description="Flight Control User Interface Service" \
+ io.k8s.display-name="Flight Control UI" \
+ name="flightctl-ui" \
+ summary="Flight Control User Interface Service"
+EXPOSE 8080
+CMD ["./flightctl-ui"]
diff --git a/packaging/images/el10/Containerfile.ocp b/packaging/images/el10/Containerfile.ocp
new file mode 100644
index 000000000..b138dbc9a
--- /dev/null
+++ b/packaging/images/el10/Containerfile.ocp
@@ -0,0 +1,35 @@
+FROM registry.access.redhat.com/ubi10/nodejs-22-minimal:latest as ui-build
+USER root
+RUN microdnf install -y rsync
+
+WORKDIR /app
+COPY package.json /app
+COPY package-lock.json /app
+COPY tsconfig.json /app
+COPY libs /app/libs
+COPY apps /app/apps
+ENV NODE_OPTIONS='--max-old-space-size=8192'
+RUN npm ci
+ARG PLUGIN_VERSION=""
+ENV PLUGIN_VERSION=$PLUGIN_VERSION
+RUN npm run build:ocp
+
+FROM registry.access.redhat.com/ubi10/go-toolset:1.24.6-1763548447 as proxy-build
+WORKDIR /app
+COPY proxy /app
+USER 0
+RUN CGO_ENABLED=1 CGO_CFLAGS=-flto GOEXPERIMENT=strictfipsruntime go build
+
+FROM quay.io/flightctl/flightctl-base:el10-10.1-1769518576
+COPY --from=ui-build /app/apps/ocp-plugin/dist /app/proxy/dist
+COPY --from=proxy-build /app/flightctl-ui /app/proxy
+WORKDIR /app/proxy
+LABEL \
+ com.redhat.component="flightctl-ui-ocp-container" \
+ description="Flight Control User Interface Service for OCP Integration" \
+ io.k8s.description="Flight Control User Interface Service for OCP Integration" \
+ io.k8s.display-name="Flight Control UI (OCP)" \
+ name="flightctl-ui-ocp" \
+ summary="Flight Control User Interface Service for OCP Integration"
+EXPOSE 8080
+CMD ["./flightctl-ui"]
diff --git a/Containerfile b/packaging/images/el9/Containerfile
similarity index 97%
rename from Containerfile
rename to packaging/images/el9/Containerfile
index 92427f845..42d8c98c6 100644
--- a/Containerfile
+++ b/packaging/images/el9/Containerfile
@@ -30,4 +30,4 @@ LABEL \
name="flightctl-ui" \
summary="Flight Control User Interface Service"
EXPOSE 8080
-CMD ["./flightctl-ui"]
+CMD ["./flightctl-ui"]
\ No newline at end of file
diff --git a/Containerfile.ocp b/packaging/images/el9/Containerfile.ocp
similarity index 98%
rename from Containerfile.ocp
rename to packaging/images/el9/Containerfile.ocp
index 795fc3bed..1dd17afb4 100644
--- a/Containerfile.ocp
+++ b/packaging/images/el9/Containerfile.ocp
@@ -32,4 +32,4 @@ LABEL \
name="flightctl-ui-ocp" \
summary="Flight Control User Interface Service for OCP Integration"
EXPOSE 8080
-CMD ["./flightctl-ui"]
+CMD ["./flightctl-ui"]
\ No newline at end of file