Skip to content

Commit 407100f

Browse files
Manage OS update policy page (#328)
Co-authored-by: Vignesh Chandrasekharan <vignesh.chandrasekharan@intel.com>
1 parent f78208a commit 407100f

21 files changed

Lines changed: 1953 additions & 0 deletions

apps/admin/src/components/templates/Layout.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ export const osProfilesNavItem: CollapsableListItem<string> = {
5050
divider: true,
5151
};
5252

53+
export const osUpdatePolicyNavItem: CollapsableListItem<string> = {
54+
route: "os-update-policy",
55+
icon: "globe",
56+
value: "OS Update Policy",
57+
divider: true,
58+
};
59+
5360
export const projectsNavItem: CollapsableListItem<string> = {
5461
route: "projects",
5562
icon: "globe",
@@ -123,6 +130,7 @@ const Layout = () => {
123130
checkAuthAndRole([Role.INFRA_MANAGER_READ, Role.INFRA_MANAGER_WRITE])
124131
) {
125132
items.push(osProfilesNavItem);
133+
items.push(osUpdatePolicyNavItem);
126134
items.push(sshNavItem);
127135
}
128136

apps/admin/src/remotes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ declare module "EimUI/HostLink";
1010
declare module "EimUI/OSProfiles";
1111
declare module "EimUI/HostsTable";
1212
declare module "EimUI/AggregateHostStatus";
13+
declare module "EimUI/OsUpdatePolicy";

apps/admin/src/routes/routes.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type RemoteComponent = LazyExoticComponent<ComponentType<any>> | null;
2929
let ClusterTemplates: RemoteComponent = null;
3030
let ClusterTemplateDetails: RemoteComponent = null;
3131
let OSProfiles: RemoteComponent = null;
32+
let OsUpdatePolicy: RemoteComponent = null;
3233

3334
if (RuntimeConfig.isEnabled("CLUSTER_ORCH")) {
3435
ClusterTemplates = React.lazy(
@@ -41,6 +42,7 @@ if (RuntimeConfig.isEnabled("CLUSTER_ORCH")) {
4142

4243
if (RuntimeConfig.isEnabled("INFRA")) {
4344
OSProfiles = React.lazy(async () => await import("EimUI/OSProfiles"));
45+
OsUpdatePolicy = React.lazy(async () => await import("EimUI/OsUpdatePolicy"));
4446
}
4547

4648
const isProjectAdmin = hasRole([
@@ -79,6 +81,10 @@ const getHomeRoute = () => {
7981
return "/admin/os-profiles";
8082
}
8183

84+
if (OsUpdatePolicy && hasInfraPermission) {
85+
return "/admin/os-update-policy";
86+
}
87+
8288
return "/admin/about";
8389
};
8490

@@ -171,6 +177,15 @@ if (RuntimeConfig.isEnabled("INFRA")) {
171177
}
172178
}
173179

180+
if (RuntimeConfig.isEnabled("INFRA")) {
181+
if (OsUpdatePolicy) {
182+
childRoutes.push({
183+
path: "os-update-policy",
184+
element: <OsUpdatePolicy />,
185+
});
186+
}
187+
}
188+
174189
const routes: RouteObject[] = [
175190
{
176191
path: "/",

apps/infra/config/webpack.common.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ module.exports = {
5454
"./SiteCellRemote": "./src/components/atom/SiteCell/SiteCellRemote",
5555
"./HostLink": "./src/components/atom/HostLink/HostLinkRemote",
5656
"./OSProfiles": "./src/components/pages/OSProfiles/OSProfilesRemote",
57+
"./OsUpdatePolicy":
58+
"./src/components/pages/OsUpdatePolicy/OsUpdatePolicyRemote",
5759
"./AggregateHostStatus":
5860
"./src/components/atom/AggregateHostStatus/AggregateHostStatusRemote",
5961
"./RegionSiteTree":
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
* SPDX-FileCopyrightText: (C) 2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { cyGet } from "@orch-ui/tests";
7+
import { osUbuntu } from "@orch-ui/utils";
8+
import CreateOsUpdatePolicyDrawer from "./CreateOsUpdatePolicyDrawer";
9+
import { CreateOsUpdatePolicyDrawerPom } from "./CreateOsUpdatePolicyDrawer.pom";
10+
11+
const pom = new CreateOsUpdatePolicyDrawerPom();
12+
13+
describe("<CreateOsUpdatePolicyDrawer/>", () => {
14+
let mockProps: any;
15+
16+
beforeEach(() => {
17+
// Create fresh stubs for each test
18+
mockProps = {
19+
showDrawer: true,
20+
setShowDrawer: cy.stub(),
21+
onPolicyCreated: cy.stub(),
22+
showToast: cy.stub(),
23+
};
24+
});
25+
26+
describe("Basic Functionality", () => {
27+
beforeEach(() => {
28+
pom.interceptApis([pom.api.getOperatingSystems]);
29+
cy.mount(<CreateOsUpdatePolicyDrawer {...mockProps} />);
30+
pom.waitForApis();
31+
});
32+
33+
it("should render drawer component", () => {
34+
pom.root.should("exist");
35+
cy.contains("Create OS Update Policy").should("exist");
36+
});
37+
38+
it("should show required fields", () => {
39+
pom.el.name.should("exist");
40+
pom.el.description.should("exist");
41+
pom.el.osType.should("exist");
42+
pom.el.updatePolicy.should("exist");
43+
});
44+
45+
it("should have default values set", () => {
46+
// OS Type should default to Mutable
47+
pom.el.osType.should("contain", "Mutable OS");
48+
// Update Policy should default to Latest
49+
pom.el.updatePolicy.should("contain", "Update To Latest");
50+
});
51+
});
52+
53+
describe("Form Validation", () => {
54+
beforeEach(() => {
55+
pom.interceptApis([pom.api.getOperatingSystems]);
56+
cy.mount(<CreateOsUpdatePolicyDrawer {...mockProps} />);
57+
pom.waitForApis();
58+
});
59+
60+
it("should show validation error for empty name", () => {
61+
cy.get('[data-cy="addBtn"]').click();
62+
cy.contains("Name is required").should("exist");
63+
});
64+
65+
it("should validate name length", () => {
66+
const longName = "a".repeat(51);
67+
cy.get('[data-cy="name"]').type(longName);
68+
cy.get('[data-cy="addBtn"]').click();
69+
cy.contains("Name must be less than 50 characters").should("exist");
70+
});
71+
72+
// it("should validate package names format", () => {
73+
// // Switch to Mutable OS and Update To Target to show package field
74+
// pom.selectOsType("OS_TYPE_MUTABLE");
75+
// pom.selectUpdatePolicy("UPDATE_POLICY_TARGET");
76+
77+
// // Enter invalid package name with version
78+
// cy.get('[data-cy="installPackages"]').type("package=1.0.0");
79+
// cy.get('[data-cy="addBtn"]').click();
80+
81+
// cy.contains(
82+
// "Package names should not contain version information",
83+
// ).should("exist");
84+
// });
85+
86+
it("should validate update sources format", () => {
87+
// Switch to Mutable OS and Update To Target
88+
pom.selectOsType("OS_TYPE_MUTABLE");
89+
pom.selectUpdatePolicy("UPDATE_POLICY_TARGET");
90+
91+
// Enter invalid source format
92+
cy.get('[data-cy="updateSources"]').type("invalid-source-format");
93+
cy.get('[data-cy="addBtn"]').click();
94+
95+
cy.contains("Repository sources must start with 'deb'").should("exist");
96+
});
97+
});
98+
99+
describe("Conditional Field Rendering", () => {
100+
beforeEach(() => {
101+
pom.interceptApis([pom.api.getOperatingSystems]);
102+
cy.mount(<CreateOsUpdatePolicyDrawer {...mockProps} />);
103+
pom.waitForApis();
104+
});
105+
106+
it("should hide advanced fields for Mutable OS with Update To Latest", () => {
107+
pom.selectOsType("OS_TYPE_MUTABLE");
108+
pom.selectUpdatePolicy("UPDATE_POLICY_LATEST");
109+
110+
// Advanced fields should not be visible
111+
cyGet("kernelCommand").should("not.exist");
112+
cyGet("installPackages").should("not.exist");
113+
cyGet("updateSources").should("not.exist");
114+
cyGet("targetOs").should("not.exist");
115+
});
116+
117+
it("should show advanced fields for Mutable OS with Update To Target", () => {
118+
pom.selectOsType("OS_TYPE_MUTABLE");
119+
pom.selectUpdatePolicy("UPDATE_POLICY_TARGET");
120+
121+
// Advanced fields should be visible
122+
cyGet("kernelCommand").should("exist");
123+
cyGet("installPackages").should("exist");
124+
cyGet("updateSources").should("exist");
125+
cyGet("targetOs").should("not.exist");
126+
});
127+
128+
it("should show target OS field for Immutable OS with Update To Target", () => {
129+
pom.selectOsType("OS_TYPE_IMMUTABLE");
130+
pom.selectUpdatePolicy("UPDATE_POLICY_TARGET");
131+
132+
// Target OS field should be visible
133+
cyGet("targetOs").should("exist");
134+
// Advanced fields should not be visible
135+
cyGet("kernelCommand").should("not.exist");
136+
cyGet("installPackages").should("not.exist");
137+
cyGet("updateSources").should("not.exist");
138+
});
139+
140+
it("should hide all advanced fields for Immutable OS with Update To Latest", () => {
141+
pom.selectOsType("OS_TYPE_IMMUTABLE");
142+
pom.selectUpdatePolicy("UPDATE_POLICY_LATEST");
143+
144+
// No advanced fields should be visible
145+
cyGet("kernelCommand").should("not.exist");
146+
cyGet("installPackages").should("not.exist");
147+
cyGet("updateSources").should("not.exist");
148+
cyGet("targetOs").should("not.exist");
149+
});
150+
});
151+
152+
describe("Field Reset Functionality", () => {
153+
beforeEach(() => {
154+
pom.interceptApis([pom.api.getOperatingSystems]);
155+
cy.mount(<CreateOsUpdatePolicyDrawer {...mockProps} />);
156+
pom.waitForApis();
157+
});
158+
159+
it("should reset fields when OS type changes", () => {
160+
// Fill in some data
161+
pom.el.name.type("Test Policy");
162+
pom.el.description.type("Test Description");
163+
164+
// Switch to show advanced fields
165+
pom.selectOsType("OS_TYPE_MUTABLE");
166+
pom.selectUpdatePolicy("UPDATE_POLICY_TARGET");
167+
pom.el.kernelCommand.type("test-kernel-command");
168+
169+
// Change OS type
170+
pom.selectOsType("OS_TYPE_IMMUTABLE");
171+
172+
// Name and description should be preserved
173+
pom.el.name.should("have.value", "Test Policy");
174+
pom.el.description.should("have.value", "Test Description");
175+
176+
// Update policy should be reset to default
177+
pom.el.updatePolicy.should("contain", "Update To Latest");
178+
});
179+
});
180+
181+
describe("Form Submission", () => {
182+
beforeEach(() => {
183+
pom.interceptApis([
184+
pom.api.getOperatingSystems,
185+
pom.api.createOsUpdatePolicy,
186+
]);
187+
cy.mount(<CreateOsUpdatePolicyDrawer {...mockProps} />);
188+
pom.waitForApis();
189+
});
190+
});
191+
192+
describe("Target OS Selection", () => {
193+
beforeEach(() => {
194+
pom.interceptApis([pom.api.getOperatingSystems]);
195+
cy.mount(<CreateOsUpdatePolicyDrawer {...mockProps} />);
196+
pom.waitForApis();
197+
});
198+
199+
it("should populate target OS options for Immutable OS", () => {
200+
pom.selectOsType("OS_TYPE_IMMUTABLE");
201+
pom.selectUpdatePolicy("UPDATE_POLICY_TARGET");
202+
203+
// Should show target OS dropdown
204+
cyGet("targetOs").should("exist");
205+
206+
// Click to open dropdown and check options
207+
cyGet("targetOs").find("button").click();
208+
cy.contains(osUbuntu.name || "").should("exist");
209+
});
210+
});
211+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* SPDX-FileCopyrightText: (C) 2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { CyApiDetails, CyPom, defaultActiveProject } from "@orch-ui/tests";
7+
import { OsResourceStore, OsUpdatePolicyStore } from "@orch-ui/utils";
8+
9+
const dataCySelectors = [
10+
"name",
11+
"description",
12+
"osType",
13+
"kernelCommand",
14+
"installPackages",
15+
"updateSources",
16+
"updatePolicy",
17+
"targetOs",
18+
"cancelFooterBtn",
19+
"addBtn",
20+
] as const;
21+
22+
type Selectors = (typeof dataCySelectors)[number];
23+
24+
const osUpdatePolicyStore = new OsUpdatePolicyStore();
25+
const osResourceStore = new OsResourceStore();
26+
27+
type ApiAliases =
28+
| "getOperatingSystems"
29+
| "createOsUpdatePolicy"
30+
| "createOsUpdatePolicyError";
31+
32+
const endpoints: CyApiDetails<ApiAliases> = {
33+
getOperatingSystems: {
34+
route: `**/v1/projects/${defaultActiveProject.name}/compute/os*`,
35+
response: {
36+
OperatingSystemResources: osResourceStore.list(),
37+
totalElements: osResourceStore.resources.length,
38+
hasNext: false,
39+
},
40+
},
41+
createOsUpdatePolicy: {
42+
method: "POST",
43+
route: `**/v1/projects/${defaultActiveProject.name}/os-update-policies`,
44+
statusCode: 200,
45+
response: (req: any) => {
46+
const newPolicy = osUpdatePolicyStore.convert(req.body);
47+
return newPolicy;
48+
},
49+
},
50+
createOsUpdatePolicyError: {
51+
method: "POST",
52+
route: `**/v1/projects/${defaultActiveProject.name}/os-update-policies`,
53+
statusCode: 400,
54+
},
55+
};
56+
57+
export class CreateOsUpdatePolicyDrawerPom extends CyPom<
58+
Selectors,
59+
ApiAliases
60+
> {
61+
constructor(public rootCy: string = "createOsUpdatePolicy") {
62+
super(rootCy, [...dataCySelectors], endpoints);
63+
}
64+
65+
selectOsType(osType: string) {
66+
this.el.osType.find("button").click({ force: true });
67+
const optionText =
68+
osType === "OS_TYPE_MUTABLE" ? "Mutable OS" : "Immutable OS";
69+
cy.get("[role='listbox']")
70+
.should("be.visible")
71+
.contains(optionText)
72+
.click({ force: true });
73+
}
74+
75+
selectUpdatePolicy(policy: string) {
76+
this.el.updatePolicy.find("button").click({ force: true });
77+
const optionText =
78+
policy === "UPDATE_POLICY_LATEST"
79+
? "Update To Latest"
80+
: "Update To Target";
81+
cy.get("[role='listbox']")
82+
.should("be.visible")
83+
.contains(optionText)
84+
.click({ force: true });
85+
}
86+
87+
fillBasicForm() {
88+
this.el.name.type("Test Policy");
89+
this.el.description.type("Test Description");
90+
}
91+
92+
fillCompleteForm() {
93+
this.fillBasicForm();
94+
this.selectOsType("OS_TYPE_MUTABLE");
95+
this.selectUpdatePolicy("UPDATE_POLICY_TARGET");
96+
this.el.kernelCommand.type("console=ttyS0,115200");
97+
this.el.installPackages.type("curl, wget, vim");
98+
this.el.updateSources.type(
99+
"deb http://archive.ubuntu.com/ubuntu focal main",
100+
);
101+
}
102+
}

0 commit comments

Comments
 (0)