aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/io/github/jshipit/ContainerManager.java
blob: d97f1f32b343ae31e8ce70e0336d5b1a24cf04e5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
// SPDX-License-Identifier: GPL-3.0-only

package io.github.jshipit;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class ContainerManager {
    private String containerName;
    private String containerRepo;
    private String containerImage;
    private String containerTag;
    private String containerApiRepo;
    private String containerCommand;
    private List<String> containerMounts;
    private boolean containerIsolated;
    private OCIDataStore dataStore;

    public ContainerManager(String containerName, String containerImage, String containerTag, String containerApiRepo, String containerRepo, boolean containerIsolated, OCIDataStore dataStore) {
        this.containerName = containerName;
        this.containerImage = containerImage;
        this.containerTag = containerTag;
        this.containerApiRepo = containerApiRepo;
        this.containerRepo = containerRepo;
        this.containerMounts = null;
        this.containerIsolated = containerIsolated;
        this.dataStore = dataStore;
    }

    public ContainerManager(String containerName, String containerCommand, List<String> containerMounts, OCIDataStore dataStore) {
        this.containerName = containerName;
        this.containerImage = dataStore.getContainerImage(containerName);
        this.containerTag = dataStore.getContainerTag(containerName);
        this.containerApiRepo = dataStore.getContainerApiRepo(containerName);
        this.containerRepo = dataStore.getContainerRepo(containerName);
        this.containerCommand = containerCommand;
        this.containerMounts = containerMounts;
        this.dataStore = dataStore;
    }

    /*
     * Create a container directory
     */
    public void createContainer() {
        System.out.println("Creating container");

        if (dataStore.containerExists(this.containerName)) {
            System.out.println("Container already exists");
            return;
        }

        if (!Files.isDirectory(Paths.get(dataStore.getPath() + "/" + this.containerImage + "/" + this.containerTag))) {
            System.out.println("Image does not exist");
            return;
        }

        String containerDirectory = dataStore.createContainerDirectory(this.containerImage, this.containerTag, this.containerName, this.containerApiRepo, this.containerRepo);

        new File(containerDirectory + "/containerOverlay").mkdirs(); // The upper directory of the overlay mount, user data and any changes to root will be stored here
        new File(containerDirectory + "/root").mkdirs(); // The root directory of the overlay mount
        new File(containerDirectory+"/work").mkdirs(); // The work directory of the overlay mount

        // Create the container TOML config file
        try {
            new File(containerDirectory + "/config.toml").createNewFile();
            FileOutputStream fos = new FileOutputStream(containerDirectory + "/config.toml");

            String str = "[container]\n";
            str += "command = \"" + this.containerCommand + "\"\n";
            str += "hostname = \"" + this.containerName + "\"\n\n";
            str += "[permissions]\n";
            str += "unshare-all = false\n";
            str += "unshare-net = false\n";
            str += "unshare-user = false\n";
            str += "unshare-ipc = false\n";
            str += "unshare-pid = false\n";
            str += "unshare-uts = true\n";
            str += "unshare-cgroup = false\n";
            str += "mount-dev = true\n";
            str += "mount-proc = true\n";
            str += "mount-bind = []\n";

            byte[] b = str.getBytes();
            fos.write(b);
            fos.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /*
     * Start a container
     *
     * @param getCommand: If true, return the command to run the container
     *
     * @return: The command to run the container. If getCommand is false, return null
     */
    public String startContainer(boolean getCommand) {

        String containerDirectory = dataStore.getContainerPath(this.containerName);
        List<String> content = null;
        try {
            content = Files.readAllLines(Paths.get(this.dataStore.getPath() + "/" + this.containerImage + "/" + this.containerTag + "/layers"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        assert content != null; // assert my beloved

        // We parse the layers file to get the layers of the image
        List<String> layers = new ArrayList<>();
        for (String line : content) {
            layers.add(this.dataStore.getPath() + "/blobs/" + line.replace("sha256:", ""));
        }
        assert layers.size() > 0;

        SysUtils sysUtils = new SysUtils();
        String[] diffs = layers.toArray(new String[0]);
        // Depending on getCommand we either directly mount the layers or return the command to mount the layers
        return sysUtils.overlayMount(diffs, containerDirectory + "/containerOverlay", containerDirectory + "/root", containerDirectory + "/work", !getCommand);

    }

    /*
     * Run a command in a container
     */
    public void runCommand() {
        String containerDirectory = dataStore.getContainerPath(this.containerName);
        String dataStorePath = dataStore.getPath();
        ConfigParser configParser = new ConfigParser(containerDirectory+"/config.toml");

        File configPath = new File(dataStorePath + "/" + this.containerImage + "/" + this.containerTag + "/config"); // Path to the config file of the image
        String content = null;
        try {
            content = Files.readAllLines(Paths.get(configPath.getAbsolutePath())).toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        ObjectMapper mapper = new ObjectMapper();
        List<String> env = new ArrayList<>();
        String cmd = null;
        String hostname = null;
        try {
            JsonNode config = mapper.readTree(content.toString()).get(0).get("config");
            config.get("Env").forEach((JsonNode envVar) -> { // Get the environment variables specified in the config file
                env.add(envVar.asText());
            });
            cmd = configParser.getString("container.command").equals("null") ? config.get("Cmd").get(0).asText() : configParser.getString("container.command"); // Get the command specified in the config file
            hostname = configParser.getString("container.hostname").equals("null") ? config.get("Hostname").asText() : configParser.getString("container.hostname"); // Get the hostname specified in the config file
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }

        // why would these be null? I don't know, but I won't bother handling this better
        assert hostname != null;
        assert cmd != null;

        List<String> bwrapCommand = new ArrayList<>();

        for(String envVar : env) {
            bwrapCommand.add("--setenv "+envVar.split("=")[0]+" "+envVar.split("=")[1]); // Create the flags for bwrap to set the environment variables
        }

        bwrapCommand.add("--bind "+containerDirectory+"/root / --chdir /"); // Bind the root to / inside the bwrap namespace

        if (!configParser.getBoolean("permissions.unshare-all")) {
            processBwrapPermissions(bwrapCommand);// Process the permissions specified in the config file
        }

        bwrapCommand.add("/bin/sh -c '"+(this.containerCommand != null ? this.containerCommand : cmd)+"'"); // Run the command specified in the config file or the command specified in the constructor

        SysUtils sysUtils = new SysUtils();
        String bwrapCMD = sysUtils.execInBwrap(bwrapCommand.toArray(new String[0]), false);
        String mountCMD = startContainer(true);

        String CMD = "unshare --user --map-root-user --mount bash -c \""+mountCMD+";"+bwrapCMD+"\""; // I am starting to realize that this project was not a good idea
        ProcessBuilder pb = new ProcessBuilder("bash", "-c", CMD);
        pb.inheritIO();
        try {
            Process p = pb.start();
            p.waitFor();
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private void processBwrapPermissions(List<String> bwrapCommand) {
        ConfigParser configParser = new ConfigParser(dataStore.getContainerPath(this.containerName)+"/config.toml");
        List<String> configMounts = configParser.getList("permissions.mount-bind");
        if (configMounts.size() > 0) {
            for (String mount : configMounts) {
                bwrapCommand.add("--bind "+mount.split(":")[0]+" "+mount.split(":")[1]); // Bind the mount to the destination specified in the config file
            }
        }
        if (this.containerMounts != null) {
            for (String mount : containerMounts) {
                bwrapCommand.add("--bind "+mount.split(":")[0]+" "+mount.split(":")[1]); // Bind the mount to the destination specified by the user
            }
        }


        if (!configParser.getBoolean("permissions.unshare-net")) {
            bwrapCommand.add("--ro-bind /etc/resolv.conf /etc/resolv.conf"); // Bind the host resolv.conf to the container
            bwrapCommand.add("--share-net"); // Share the network namespace
        } else {
            bwrapCommand.add("--unshare-net"); // Unshare the network namespace
        }

        if (configParser.getBoolean("permissions.unshare-uts")) {
            bwrapCommand.add("--unshare-uts"); // Unshare the UTS namespace

            bwrapCommand.add("--hostname "+(configParser.getString("container.hostname").equals("null") ? this.containerName : configParser.getString("container.hostname"))); // Set the hostname
        }

        if (configParser.getBoolean("permissions.unshare-user")) {
            bwrapCommand.add("--unshare-user"); // Unshare the user namespace
        }

        if (configParser.getBoolean("permissions.unshare-ipc")) {
            bwrapCommand.add("--unshare-ipc"); // Unshare the IPC namespace
        }

        if (configParser.getBoolean("permissions.unshare-pid")) {
            bwrapCommand.add("--unshare-pid"); // Unshare the PID namespace
        }

        if (configParser.getBoolean("permissions.unshare-uts")) {
            bwrapCommand.add("--unshare-uts"); // Unshare the UTS namespace
        }

        if (configParser.getBoolean("permissions.unshare-cgroup")) {
            bwrapCommand.add("--unshare-cgroup"); // Unshare the cgroup namespace
        }

        if (configParser.getBoolean("permissions.mount-dev")) {
            bwrapCommand.add("--dev-bind /dev /dev"); // Mount /dev
        } else {
	    bwrapCommand.add("--dev /dev"); // Make sure a seperate devfs exists
	}

        if (configParser.getBoolean("permissions.mount-proc")) {
            bwrapCommand.add("--bind /proc /proc"); // Mount /proc
        } else {
	    bwrapCommand.add("--proc /proc"); // Make sure a seperate procfs exists
	}
    }

    /*
     * Delete a container
     */
    public void deleteContainer() {
        String containerDirectory = dataStore.getContainerPath(this.containerName);
        try {
            Files.delete(Paths.get(containerDirectory + "/containerOverlay"));
            Files.delete(Paths.get(containerDirectory + "/root"));
            Files.delete(Paths.get(containerDirectory + "/work"));
            Files.delete(Paths.get(containerDirectory));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        dataStore.deleteContainerFromDatabase(this.containerName);
    }
}