Skip to content

Commit a9b6765

Browse files
committed
Add apt install and yum
1 parent 745c6da commit a9b6765

File tree

12 files changed

+778
-1
lines changed

12 files changed

+778
-1
lines changed

.cirrus.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ integration_individual_test_task:
4848

4949
integration_individual_test_linux_task:
5050
arm_container:
51-
image: kevinwang5658/codify-test-linux:latest
51+
image: kevinwang5658/codify-test-linux-centos:latest
5252
# node_modules_cache:
5353
# folder: node_modules
5454
# fingerprint_script: cat package-lock.json

scripts/Dockerfile renamed to scripts/docker-files/debian-linux/Dockerfile

File renamed without changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Use a CentOS base image
2+
FROM rockylinux:9
3+
4+
# Install necessary packages to fetch the NodeSource script and Node.js
5+
#RUN dnf clean all \
6+
# && rm -rf /var/cache/dnf \
7+
# && dnf -y --refresh update
8+
#
9+
#RUN dnf install sudo -y \
10+
# && dnf install nodejs npm -y
11+
12+
RUN dnf -y install which && dnf clean all
13+
RUN dnf -y install sudo && dnf clean all
14+
15+
# Update base + install deps
16+
#RUN dnf clean all \
17+
# && rm -rf /var/cache/dnf \
18+
# && dnf -y --refresh update \
19+
# && dnf -y install ca-certificates \
20+
# && dnf clean all
21+
22+
# Add NodeSource repo for Node.js 22
23+
RUN curl -fsSL https://rpm.nodesource.com/setup_22.x | bash -
24+
25+
# Install Node.js
26+
RUN dnf -y install nodejs \
27+
&& dnf clean all
28+
29+
# Verify
30+
RUN node --version && npm --version
31+
32+
## Create node user with home directory and bash shell
33+
RUN useradd --create-home --shell /bin/bash node
34+
RUN usermod -aG wheel node
35+
36+
## Ensure node owns its home directory
37+
RUN chown -R node:node /home/node
38+
39+
RUN echo "node ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/node
40+
##
41+
### Switch to node user
42+
#USER node
43+
44+
RUN dnf clean all \
45+
&& rm -rf /var/cache/dnf \
46+
&& dnf clean all

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import { SshKeyResource } from './resources/ssh/ssh-key.js';
3636
import { TerraformResource } from './resources/terraform/terraform.js';
3737
import { VscodeResource } from './resources/vscode/vscode.js';
3838
import { XcodeToolsResource } from './resources/xcode-tools/xcode-tools.js';
39+
import { AptResource } from './resources/apt/apt.js';
40+
import { YumResource } from './resources/yum/yum.js';
3941

4042
runPlugin(Plugin.create(
4143
'default',
@@ -76,5 +78,7 @@ runPlugin(Plugin.create(
7678
new Npm(),
7779
new NpmLoginResource(),
7880
new DockerResource(),
81+
new AptResource(),
82+
new YumResource(),
7983
])
8084
)

src/resources/apt/apt-schema.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "https://www.codifycli.com/apt.json",
4+
"$comment": "https://docs.codifycli.com/core-resources/apt/",
5+
"title": "Apt resource",
6+
"description": "Manage apt packages on Debian-based systems.",
7+
"type": "object",
8+
"properties": {
9+
"install": {
10+
"type": "array",
11+
"description": "Installs packages using apt.",
12+
"items": {
13+
"oneOf": [
14+
{ "type": "string" },
15+
{
16+
"type": "object",
17+
"properties": {
18+
"name": { "type": "string" },
19+
"version": { "type": "string" }
20+
},
21+
"required": ["name"]
22+
}
23+
]
24+
}
25+
},
26+
"update": {
27+
"type": "boolean",
28+
"description": "Whether to run apt-get update before installing packages. Defaults to true."
29+
}
30+
},
31+
"additionalProperties": false
32+
}

src/resources/apt/apt.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { CreatePlan, Resource, ResourceSettings, SpawnStatus, getPty } from 'codify-plugin-lib';
2+
import { OS, ResourceConfig } from 'codify-schemas';
3+
4+
import schema from './apt-schema.json';
5+
import { AptInstallParameter, AptPackage } from './install-parameter.js';
6+
7+
export interface AptConfig extends ResourceConfig {
8+
install: Array<AptPackage | string>;
9+
update?: boolean;
10+
}
11+
12+
export class AptResource extends Resource<AptConfig> {
13+
14+
override getSettings(): ResourceSettings<AptConfig> {
15+
return {
16+
id: 'apt',
17+
operatingSystems: [OS.Linux],
18+
schema,
19+
parameterSettings: {
20+
install: { type: 'stateful', definition: new AptInstallParameter() },
21+
update: { type: 'boolean', default: true, setting: true }
22+
}
23+
};
24+
}
25+
26+
override async refresh(parameters: Partial<AptConfig>): Promise<Partial<AptConfig> | null> {
27+
const $ = getPty();
28+
29+
const aptCheck = await $.spawnSafe('which apt-get');
30+
if (aptCheck.status === SpawnStatus.ERROR) {
31+
return null;
32+
}
33+
34+
return parameters;
35+
}
36+
37+
override async create(_plan: CreatePlan<AptConfig>): Promise<void> {
38+
const $ = getPty();
39+
40+
// Update package lists
41+
await $.spawn('apt-get update', { requiresRoot: true, interactive: true });
42+
43+
console.log('apt is already installed on this Debian-based system');
44+
}
45+
46+
override async destroy(): Promise<void> {
47+
// apt is a core system component and should not be removed
48+
throw new Error('apt cannot be destroyed as it is a core system package manager');
49+
}
50+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { ParameterSetting, Plan, StatefulParameter, getPty } from 'codify-plugin-lib';
2+
3+
import { AptConfig } from './apt.js';
4+
5+
export interface AptPackage {
6+
name: string;
7+
version?: string;
8+
}
9+
10+
export class AptInstallParameter extends StatefulParameter<AptConfig, Array<AptPackage | string>> {
11+
12+
getSettings(): ParameterSetting {
13+
return {
14+
type: 'array',
15+
filterInStatelessMode: (desired, current) =>
16+
current.filter((c) => desired.some((d) => this.isSamePackage(d, c))),
17+
isElementEqual: this.isEqual,
18+
}
19+
}
20+
21+
async refresh(desired: Array<AptPackage | string> | null, _config: Partial<AptConfig>): Promise<Array<AptPackage | string> | null> {
22+
const $ = getPty()
23+
const { data: installed } = await $.spawnSafe('dpkg-query -W -f=\'${Package} ${Version}\\n\'');
24+
25+
if (!installed || installed === '') {
26+
return null;
27+
}
28+
29+
const r = installed.split(/\n/)
30+
.filter(Boolean)
31+
.map((l) => {
32+
const [name, version] = l.split(/\s+/)
33+
.filter(Boolean)
34+
35+
return { name, version }
36+
})
37+
.filter((pkg) =>
38+
// Only return packages that are in the desired list
39+
desired?.some((d) => {
40+
if (typeof d === 'string') {
41+
return d === pkg.name;
42+
}
43+
44+
return d.name === pkg.name;
45+
})
46+
)
47+
.map((installed) => {
48+
if (desired?.find((d) => typeof d === 'string' && d === installed.name)) {
49+
return installed.name;
50+
}
51+
52+
if (desired?.find((d) => typeof d === 'object' && d.name === installed.name && !d.version)) {
53+
return { name: installed.name }
54+
}
55+
56+
return installed;
57+
})
58+
59+
return r.length > 0 ? r : null;
60+
}
61+
62+
async add(valueToAdd: Array<AptPackage | string>, plan: Plan<AptConfig>): Promise<void> {
63+
await this.updateIfNeeded(plan);
64+
await this.install(valueToAdd);
65+
}
66+
67+
async modify(newValue: (AptPackage | string)[], previousValue: (AptPackage | string)[], plan: Plan<AptConfig>): Promise<void> {
68+
const valuesToAdd = newValue.filter((n) => !previousValue.some((p) => this.isSamePackage(n, p)));
69+
const valuesToRemove = previousValue.filter((p) => !newValue.some((n) => this.isSamePackage(n, p)));
70+
71+
await this.uninstall(valuesToRemove);
72+
await this.updateIfNeeded(plan);
73+
await this.install(valuesToAdd);
74+
}
75+
76+
async remove(valueToRemove: (AptPackage | string)[], _plan: Plan<AptConfig>): Promise<void> {
77+
await this.uninstall(valueToRemove);
78+
}
79+
80+
private async updateIfNeeded(plan: Plan<AptConfig>): Promise<void> {
81+
if (plan.desiredConfig?.update === false) {
82+
return;
83+
}
84+
85+
const $ = getPty();
86+
await $.spawn('apt-get update', { requiresRoot: true, interactive: true });
87+
}
88+
89+
private async install(packages: Array<AptPackage | string>): Promise<void> {
90+
if (!packages || packages.length === 0) {
91+
return;
92+
}
93+
94+
const $ = getPty();
95+
const toInstall = packages.map((p) => {
96+
if (typeof p === 'string') {
97+
return p;
98+
}
99+
100+
if (p.version) {
101+
return `${p.name}=${p.version}`;
102+
}
103+
104+
return p.name;
105+
}).join(' ');
106+
107+
await $.spawn(`apt-get install -y ${toInstall}`, { requiresRoot: true, interactive: true });
108+
}
109+
110+
private async uninstall(packages: Array<AptPackage | string>): Promise<void> {
111+
if (!packages || packages.length === 0) {
112+
return;
113+
}
114+
115+
const $ = getPty();
116+
const toUninstall = packages.map((p) => {
117+
if (typeof p === 'string') {
118+
return p;
119+
}
120+
121+
return p.name;
122+
}).join(' ');
123+
124+
await $.spawn(`apt-get remove -y ${toUninstall}`, { requiresRoot: true, interactive: true });
125+
}
126+
127+
isSamePackage(a: AptPackage | string, b: AptPackage | string): boolean {
128+
if (typeof a === 'string' || typeof b === 'string') {
129+
if (typeof a === 'string' && typeof b === 'string') {
130+
return a === b;
131+
}
132+
133+
if (typeof a === 'string' && typeof b === 'object') {
134+
return a === b.name;
135+
}
136+
137+
if (typeof a === 'object' && typeof b === 'string') {
138+
return a.name === b;
139+
}
140+
}
141+
142+
if (typeof a === 'object' && typeof b === 'object') {
143+
return a.name === b.name;
144+
}
145+
146+
return false;
147+
}
148+
149+
isEqual(desired: AptPackage | string, current: AptPackage | string): boolean {
150+
if (typeof desired === 'string' || typeof current === 'string') {
151+
if (typeof desired === 'string' && typeof current === 'string') {
152+
return desired === current;
153+
}
154+
155+
if (typeof desired === 'string' && typeof current === 'object') {
156+
return desired === current.name;
157+
}
158+
159+
if (typeof desired === 'object' && typeof current === 'string') {
160+
return desired.name === current;
161+
}
162+
}
163+
164+
if (typeof desired === 'object' && typeof current === 'object') {
165+
return desired.version
166+
? desired.version === current.version && desired.name === current.name
167+
: desired.name === current.name;
168+
}
169+
170+
return false;
171+
}
172+
}

0 commit comments

Comments
 (0)