All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.oracle.bedrock.runtime.k8s.linuxkit.LinuxKitK8sCluster Maven / Gradle / Ivy

/*
 * File: LinuxKitK8sCluster.java
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * The contents of this file are subject to the terms and conditions of
 * the Common Development and Distribution License 1.0 (the "License").
 *
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the License by consulting the LICENSE.txt file
 * distributed with this file, or by consulting https://oss.oracle.com/licenses/CDDL
 *
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file LICENSE.txt.
 *
 * MODIFICATIONS:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 */

package com.oracle.bedrock.runtime.k8s.linuxkit;

import com.oracle.bedrock.io.FileHelper;

import com.oracle.bedrock.options.LaunchLogging;
import com.oracle.bedrock.options.Timeout;

import com.oracle.bedrock.runtime.Application;
import com.oracle.bedrock.runtime.ApplicationConsoleBuilder;
import com.oracle.bedrock.runtime.LocalPlatform;

import com.oracle.bedrock.runtime.SimpleApplication;
import com.oracle.bedrock.runtime.console.CapturingApplicationConsole;
import com.oracle.bedrock.runtime.console.FileWriterApplicationConsole;
import com.oracle.bedrock.runtime.console.SystemApplicationConsole;

import com.oracle.bedrock.runtime.k8s.K8sCluster;

import com.oracle.bedrock.runtime.network.AvailablePortIterator;

import com.oracle.bedrock.runtime.options.Argument;
import com.oracle.bedrock.runtime.options.Arguments;
import com.oracle.bedrock.runtime.options.Console;
import com.oracle.bedrock.runtime.options.DisplayName;
import com.oracle.bedrock.runtime.options.Executable;
import com.oracle.bedrock.runtime.options.WorkingDirectory;

import com.oracle.bedrock.runtime.remote.RemotePlatform;
import com.oracle.bedrock.runtime.remote.SecureKeys;
import com.oracle.bedrock.runtime.remote.options.Deployer;
import com.oracle.bedrock.runtime.remote.options.StrictHostChecking;
import com.oracle.bedrock.runtime.remote.options.UserKnownHostsFile;

import com.oracle.bedrock.testsupport.deferred.Eventually;

import com.oracle.bedrock.util.Capture;
import com.oracle.bedrock.util.Pair;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;

import java.net.InetAddress;

import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;

import java.util.logging.Logger;

import java.util.stream.Collectors;

import static com.oracle.bedrock.deferred.DeferredHelper.invoking;
import static org.hamcrest.CoreMatchers.is;

/**
 * A Kubernetes cluster running on a LinuxKit cluster.
 * 

* Copyright (c) 2018. All Rights Reserved. Oracle Corporation.
* Oracle is a registered trademark of Oracle Corporation and/or its affiliates. * * @author Jonathan Knight */ public class LinuxKitK8sCluster extends K8sCluster implements Closeable { /** * The {@link Logger} for this class. */ private static Logger LOGGER = Logger.getLogger(LinuxKitK8sCluster.class.getName()); /** * The location of the linuxkit VM's kubectl config file. */ private static final String K8S_ADMIN_CONF = "/etc/kubernetes/admin.conf"; /** * The location of the linux kit binary. */ public static final String LINUXKIT_CMD; /** * The System property to use to set the location of the LinuxKit K8s source code. */ public static final String BEDROCK_LINUXKIT_K8S_HOME = "bedrock.linuxkit.k8s.home"; /** * The location of LinuxKit. */ private String linuxKit; /** * The location of the LinuxKit K8s source. See https://github.com/linuxkit/kubernetes */ private String linuxKitK8sHome; /** * The {@link ApplicationConsoleBuilder} to use to create consoles for the linux kit VMs. */ private ApplicationConsoleBuilder consoleBuilder = SystemApplicationConsole.builder(); /** * Temporary folder for this cluster. */ private File tempFolder; /** * The K8s Master VM {@link Application}. */ private Application appMaster; /** * The {@link RemotePlatform} linked to the master node VM. */ private RemotePlatform platformMaster; /** * The master node nat'ed ssh port. */ private Capture sshPort; /** * The master node nat'ed K8s port. */ private Capture k8sPort; /** * The location to write VM logs to. */ private File logDir; /** * The number of worker nodes to start. */ private int workerNodeCount = 2; /** * The master VM memory in MB. */ private int masterMemory = 1024; /** * The master VM memory in MB. */ private int nodeMemory = 2048; /** * The master VM disc size in GB. */ private int masterDiscSize = 4; /** * The master VM disc size in GB. */ private int nodeDiscSize = 4; /** * Flag indicating whether to clear the LinuxKit k8s VM state before starting. */ private boolean clearState = true; /** * Flag indicating whether the .iso file used by the VM should be copied * into the VM state directory due to some environments locking the file * and causing issues if multiple VMs use the same .iso file. */ private boolean copyISO = true; /** * The list of worker node VM {@link Application}s */ private final List> workerNodes = new ArrayList<>(); /** * Create a {@link LinuxKitK8sCluster}. */ public LinuxKitK8sCluster() { this(LINUXKIT_CMD, null); } /** * Create a {@link LinuxKitK8sCluster}. * * @param linuxKitK8sHome the location of the LinuxKit K8s source */ public LinuxKitK8sCluster(String linuxKitK8sHome) { this(LINUXKIT_CMD, linuxKitK8sHome); } /** * Create a {@link LinuxKitK8sCluster}. * * @param linuxKit the location of LinuxKit * @param linuxKitK8sHome the location of the LinuxKit K8s source */ public LinuxKitK8sCluster(String linuxKit, String linuxKitK8sHome) { this.linuxKit = linuxKit; this.linuxKitK8sHome = linuxKitK8sHome; if (this.linuxKitK8sHome == null || this.linuxKit.isEmpty()) { this.linuxKitK8sHome = System.getProperty(BEDROCK_LINUXKIT_K8S_HOME); } if (this.linuxKitK8sHome == null || this.linuxKitK8sHome.isEmpty()) { throw new IllegalArgumentException("LinuxKit K8s Home not provided either as a parameter of via the " + BEDROCK_LINUXKIT_K8S_HOME + " System property"); } try { tempFolder = FileHelper.createTemporaryFolder("bedrock-linuxkit"); } catch (IOException e) { throw ensureRuntimeException(e); } } /** * Start the K8s cluster. */ @Override public void start() { LOGGER.info("Starting linuxkit k8s cluster in " + linuxKitK8sHome); try { LocalPlatform platform = LocalPlatform.get(); AvailablePortIterator ports = platform.getAvailablePorts(); File k8sHome = new File(linuxKitK8sHome); String master = "kube-master-state"; File masterDir = new File(k8sHome, master); String os = System.getProperty("os.name"); String masterDisc = String.format("size=%dG", masterDiscSize); boolean efi = false; String sshKey = System.getProperty("user.home") + "/.ssh/id_rsa"; ApplicationConsoleBuilder console = consoleBuilder; String kubeEfi; String masterImage; if (logDir != null) { logDir.mkdirs(); console = FileWriterApplicationConsole.builder(logDir.getCanonicalPath(), "", ".log"); } sshPort = new Capture<>(ports); k8sPort = new Capture<>(ports); if (clearState) { LOGGER.info("Deleting previous k8s master state in " + masterDir); FileHelper.recursiveDelete(masterDir); } masterDir.mkdirs(); ensureMasterMetaData(masterDir); if ("Mac OS X".equals(os)) { efi = true; } if (efi) { masterImage = "kube-master-efi.iso"; kubeEfi = "--uefi"; } else { masterImage = "kube-master.iso"; kubeEfi = ""; } LOGGER.info("Starting linuxkit k8s master"); // start the linuxkit k8s master VM appMaster = platform.launch(linuxKit, Argument.of("run"), Argument.of("-publish", sshPort.get() + ":22"), Argument.of("-publish", k8sPort.get() + ":8443"), Argument.of("-networking", "default"), Argument.of("-cpus", "2"), Argument.of("-mem", masterMemory), Argument.of("-disk", masterDisc), Argument.of("-state", masterDir.getName()), Argument.of("-data-file", master + "/metadata.json"), Argument.of(kubeEfi), Argument.of(masterImage), WorkingDirectory.at(linuxKitK8sHome), DisplayName.of("k8s-master"), console); // create a Bedrock RemotePlatform that can be used to execute commands directly on the master VM platformMaster = new RemotePlatform("master", InetAddress.getByName("localhost"), sshPort.get(), "root", SecureKeys.fromPrivateKeyFile(sshKey), WorkingDirectory.at("/root"), Deployer.NULL, ContainerdCommandInterceptor.instance(), StrictHostChecking.disabled(), UserKnownHostsFile.at("/dev/null")); LOGGER.info("Waiting to connect to k8s master..."); // we are now basically waiting for the master VM to start by seeing if we can ssh into it Eventually.assertThat(invoking(this).canConnectTo(platformMaster), is(true), Timeout.after(2, TimeUnit.MINUTES)); LOGGER.info("Connected to k8s master"); // copy the kubectl config from the master VM to a local folder so that we // can use kubectl from the local host writeKubectlConfig(); LOGGER.info("Waiting for k8s master status to be Ready..."); // The linuxkit VMs take a while to actually start and initialize K8s // so we'll pause here to give the master node time to get going Thread.sleep(60000); // Now wait for the K8s master status to be ready Eventually.assertThat(invoking(this).isMasterReady(), is(true), Timeout.after(2, TimeUnit.MINUTES)); LOGGER.info("k8s master status is Ready"); LOGGER.info("Starting " + workerNodeCount + " k8s worker nodes..."); for (int i = 0; i < workerNodeCount; i++) { int nodeId = addWorkerNode(); // The linuxkit VMs take a while to actually start and initialize K8s // so we'll pause here to give the worker nodes time to get going Thread.sleep(30000); // Now wait for the K8s worker node status to be ready LOGGER.info("Waiting for all " + (1 + nodeId) + " k8s nodes to be ready..."); Eventually.assertThat(invoking(this).areAllNodesReady(1 + nodeId), is(true), Timeout.after(2, TimeUnit.MINUTES)); } LOGGER.info("Started " + workerNodeCount + " k8s worker nodes"); LOGGER.info("K8s Cluster is Ready"); } catch (Exception e) { throw ensureRuntimeException(e); } } @Override public void close() { for (Pair pair : workerNodes) { // send the poweroff command to the worker node VM close(pair.getX(), pair.getY()); } workerNodes.clear(); if (appMaster != null) { // send the poweroff command to the master VM close(appMaster, platformMaster); } } private void close(Application application, RemotePlatform platform) { try { platform.launch("poweroff -f"); } catch (Exception e) { e.printStackTrace(); } try { application.close(); } catch (Exception e) { e.printStackTrace(); } } /** * Add a worker node to the cluster. * * @return the id of the new node * * @throws Exception if there is an error */ public synchronized int addWorkerNode() throws Exception { LocalPlatform platform = LocalPlatform.get(); AvailablePortIterator ports = platform.getAvailablePorts(); File k8sHome = new File(linuxKitK8sHome); String os = System.getProperty("os.name"); boolean efi = false; ApplicationConsoleBuilder console = consoleBuilder; String nodeDisc = String.format("size=%dG", nodeDiscSize); String sshKey = System.getProperty("user.home") + "/.ssh/id_rsa"; String nodeImage; String kubeEfi; if (logDir != null) { logDir.mkdirs(); console = FileWriterApplicationConsole.builder(logDir.getCanonicalPath(), "", ".log"); } if ("Mac OS X".equals(os)) { efi = true; } if (efi) { nodeImage = "kube-node-efi.iso"; kubeEfi = "--uefi"; } else { nodeImage = "kube-node.iso"; kubeEfi = ""; } String masterAddress = getMasterAddress(); String joinToken = getJoinToken(); int nodeId = 1 + workerNodes.size(); String node = "kube-node-" + nodeId + "-state"; File nodeDir = new File(k8sHome, node); Capture port = new Capture<>(ports); if (clearState) { LOGGER.info("Deleting previous k8s master state in " + nodeDir); FileHelper.recursiveDelete(nodeDir); } nodeDir.mkdirs(); File fileNodeISO = new File(nodeDir, nodeImage); if (!fileNodeISO.exists()) { Files.copy(new File(k8sHome, nodeImage).toPath(), fileNodeISO.toPath()); } ensureNodeMetaData(nodeDir, joinToken, masterAddress); LOGGER.info("Starting k8s worker node " + nodeId + "..."); Application appNode = platform.launch(linuxKit, Argument.of("run"), Argument.of("-publish", port.get() + ":22"), Argument.of("-networking", "default"), Argument.of("-cpus", "2"), Argument.of("-mem", nodeMemory), Argument.of("-disk", nodeDisc), Argument.of("-state", nodeDir.getName()), Argument.of("-data-file", node + "/metadata.json"), Argument.of(kubeEfi), Argument.of(nodeDir + "/" + nodeImage), WorkingDirectory.at(linuxKitK8sHome), DisplayName.of("k8s-node-" + nodeId), console); RemotePlatform platformNode = new RemotePlatform("node-" + nodeId, InetAddress.getByName("localhost"), port.get(), "root", SecureKeys.fromPrivateKeyFile(sshKey), WorkingDirectory.at("/root"), Deployer.NULL, ContainerdCommandInterceptor.instance(), StrictHostChecking.disabled(), UserKnownHostsFile.at("/dev/null")); // we are now basically waiting for the node VM to start by seeing if we can ssh into it LOGGER.info("Waiting for k8s worker node " + nodeId + " VM to start..."); Eventually.assertThat(invoking(this).canConnectTo(platformNode), is(true), Timeout.after(2, TimeUnit.MINUTES)); LOGGER.info("K8s worker node " + nodeId + " VM started"); workerNodes.add(new Pair<>(appNode, platformNode)); return nodeId; } @Override public boolean isMasterReady() { CapturingApplicationConsole console = new CapturingApplicationConsole(); String command = "kubectl"; try (Application application = platformMaster.launch(command, Argument.of("--kubeconfig"), Argument.of(K8S_ADMIN_CONF), Arguments.of("get", "nodes"), Console.of(console), LaunchLogging.disabled())) { int exitCode = application.waitFor(); if (exitCode == 0) { String line = console.getCapturedOutputLines().stream() .filter(this::isMasterNodeLine) .findFirst() .orElse(""); LOGGER.info("Master status check: line=" + line); return "Ready".equalsIgnoreCase(getNodeStatus(line)); } else { String lines = console.getCapturedOutputLines().stream().collect(Collectors.joining("\n")) + console.getCapturedErrorLines().stream().collect(Collectors.joining("\n")); LOGGER.info("Master status check: return code=" + exitCode + " console=\n" + lines); } } catch (Exception e) { // ignored } return false; } public boolean areAllNodesReady(int nodeCount) { CapturingApplicationConsole console = new CapturingApplicationConsole(); String command = "kubectl"; try (Application application = platformMaster.launch(command, Argument.of("--kubeconfig"), Argument.of(K8S_ADMIN_CONF), Arguments.of("get", "nodes"), Console.of(console), LaunchLogging.disabled())) { int exitCode = application.waitFor(); LOGGER.info("Node status check: return code=" + exitCode + " console=\n" + console.getCapturedOutputLines().stream().collect(Collectors.joining("\n")) + console.getCapturedErrorLines().stream().collect(Collectors.joining("\n"))); if (exitCode == 0) { Queue lines = console.getCapturedOutputLines(); // pull off the header lines.poll(); // count the ready lines long readyCount = lines.stream() .filter(s -> !("(terminated)".equals(s))) // ignore the terminator line .filter(s -> "ready".equalsIgnoreCase(getNodeStatus(s))) .count(); return readyCount == nodeCount; } } catch (Exception e) { // ignored } return false; } /** * Determine whether the master node VM is running. * * @return {@code true} if the master node VM is running */ public boolean isMasterVmRunning() { return appMaster != null && appMaster.isOperational(); } /** * Obtain the {@link RemotePlatform} to use to execute * processes on the master node VM. * * @return the {@link RemotePlatform} to use to execute * processes on the master node VM */ public RemotePlatform getMasterPlatform() { if (isMasterVmRunning() && platformMaster != null) { throw new IllegalStateException("Master node is not running"); } return platformMaster; } /** * Set the {@link ApplicationConsoleBuilder} to use to build consoles for * the Linux Kit VMs. * * @param builder the {@link ApplicationConsoleBuilder} to use * * @return this {@link LinuxKitK8sCluster} */ public LinuxKitK8sCluster withConsoleBuilder(ApplicationConsoleBuilder builder) { this.consoleBuilder = builder; return this; } /** * Set the location for the VM logs. * * @param logDir the location for the VM logs * * @return this {@link LinuxKitK8sCluster} */ public LinuxKitK8sCluster withLogsAt(File logDir) { this.logDir = logDir; return this; } /** * Set the number of worker nodes to create. * * @param count the number of worker nodes * * @return this {@link LinuxKitK8sCluster} */ public LinuxKitK8sCluster withWorkerCount(int count) { workerNodeCount = count; return this; } /** * Set the number of worker nodes to create. * * @param clear {@code true} to clear the k8s LinuxKit VM state prior to starting VM instances * * @return this {@link LinuxKitK8sCluster} */ public LinuxKitK8sCluster withClearedState(boolean clear) { clearState = clear; return this; } /** * Set the flag indicating whether to copy the iso file used by the VM. * * @param copy {@code true} to make a copy of the .iso file used by the VM * * @return this {@link LinuxKitK8sCluster} */ public LinuxKitK8sCluster withIsoCopy(boolean copy) { copyISO = copy; return this; } /** * Determine whether a connection can be made to the * specified node {@link RemotePlatform}. * * @param platform the node to attempt to connect to * * @return {@code true} if a connection can be made to * the node {@link RemotePlatform} */ // must be public - used in Eventually.assertThat public boolean canConnectTo(RemotePlatform platform) { if (isMasterVmRunning()) { try (Application application = platform.launch("echo connection test", LaunchLogging.disabled())) { int exitCode = application.waitFor(); return exitCode == 0; } catch (Throwable t) { // ignored } } return false; } /** * Write the linuxkit metadata file for the master VM. * * @param masterDir the folder to write the metadata file to * * @throws IOException if an error occurs writing the metadata */ private void ensureMasterMetaData(File masterDir) throws IOException { File file = new File(masterDir, "metadata.json"); if (!file.exists()) { try (PrintWriter writer = new PrintWriter(file)) { writer.print("{ \"kubeadm\": { \"entries\": { \"init\": { \"content\": \"\" } } } }"); } } } /** * Write the linuxkit metadata file for the master VM. * * @param nodeDir the folder to write the metadata file to * @param joinToken the k8s join token to use * @param masterAddress the IP address of the master node * * @throws IOException if an error occurs writing the metadata */ private void ensureNodeMetaData(File nodeDir, String joinToken, String masterAddress) throws IOException { File file = new File(nodeDir, "metadata.json"); if (!file.exists()) { String nodeMetaData = String.format("{ \"kubeadm\": { \"entries\": { \"join\": { \"content\": " + "\"--token %s %s:6443 --discovery-token-unsafe-skip-ca-verification\" }}}}", joinToken, masterAddress); try (PrintWriter writer = new PrintWriter(file)) { writer.print(nodeMetaData); } } } /** * Write out the kubectl configuration file to use. * * @throws IOException if an error occurs */ private void writeKubectlConfig() throws IOException { LOGGER.info("Obtaining kubectl configuration from master..."); Eventually.assertThat(invoking(this).masterKubectlConfigExists(), is(true), Timeout.after(2, TimeUnit.MINUTES)); CapturingApplicationConsole console = new CapturingApplicationConsole(); String catCmd = "cat /etc/kubernetes/admin.conf"; try (Application application = platformMaster.launch(catCmd, Console.of(console))) { application.waitFor(); } File kubectlConfigFile = new File(tempFolder, "admin.conf"); try (PrintWriter writer = new PrintWriter(kubectlConfigFile)) { console.getCapturedOutputLines() .stream() .filter(line -> !line.contains("(terminated)")) .map(this::convertLine) .forEach(writer::println); } try (BufferedReader reader = new BufferedReader(new FileReader(kubectlConfigFile))) { LOGGER.info(() -> "Saved kubectl configuration to " + kubectlConfigFile + "\n" + reader.lines().collect(Collectors.joining("\n"))); } withKubectlConfig(kubectlConfigFile); } /** * Determine whether the kubectl config file exists on the master VM. * * @return {@code true} if the kubectl config file exists on the master VM */ // must be public to be used in Eventually.assertThat public boolean masterKubectlConfigExists() { try { String cmdTest = "test -f /etc/kubernetes/admin.conf"; try (Application application = platformMaster.launch(cmdTest, DisplayName.of("test"), LaunchLogging.disabled(), SystemApplicationConsole.builder())) { return application.waitFor() == 0; } } catch (Exception e) { return false; } } /** * Obtain the internal IP address of the K8s master node. * * @return the internal IP address of the K8s master node */ protected String getMasterAddress() { CapturingApplicationConsole console = new CapturingApplicationConsole(); String command = "ip -f inet -o addr show eth0"; try (Application kubectl = platformMaster.launch(command, Console.of(console), LaunchLogging.disabled())) { if (kubectl.waitFor() == 0) { String line = console.getCapturedOutputLines().poll(); String[] parts = line.split("\\s+"); String ip = parts[3]; return ip.split("/")[0]; } } return null; } /** * Obtain the join token from the k8s master node that can be used * by worker nodes to join the cluster. * * @return the join token from the k8s master node */ protected String getJoinToken() { CapturingApplicationConsole console = new CapturingApplicationConsole(); try (Application kubeadm = platformMaster.launch(SimpleApplication.class, Executable.named("kubeadm"), Arguments.of("token", "list"), Console.of(console), DisplayName.of("kubeadm"))) { if (kubeadm.waitFor() == 0) { Queue lines = console.getCapturedOutputLines(); if (lines.size() > 1) { // skip the header line lines.poll(); // read the first token line String line = lines.poll(); // return the token part return line.split("\\s+")[0]; } } } return null; } /** * Convert lines of the kubectl configuration file. * * @param line the line to convert * * @return the converted line */ private String convertLine(String line) { if (line.startsWith(" server: https://")) { // this is the server line so convert to local host and the nat'ed port return " server: https://127.0.0.1:" + k8sPort.get(); } return line; } // initialise the linuxkit location static { String linux = System.getProperty("bedrock.linuxkit"); if (linux == null || linux.isEmpty()) { linux = "/usr/local/bin/linuxkit"; } LINUXKIT_CMD = linux; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy