/// <summary> /// Constructs a configuration from a structured name. /// </summary> /// <param name="contextName">The structured context name.</param> public KubeConfigContext(KubeContextName contextName) : this() { Covenant.Requires <ArgumentNullException>(contextName != null, nameof(contextName)); this.Name = contextName.ToString(); }
/// <summary> /// Returns the named neonKUBE related context (using a structured context name). /// </summary> /// <param name="name">The raw context name.</param> /// <returns>The <see cref="KubeConfigContext"/> or <c>null</c>.</returns> public KubeConfigContext GetContext(KubeContextName name) { Covenant.Requires <ArgumentNullException>(name != null, nameof(name)); var rawName = name.ToString(); return(Contexts.SingleOrDefault(context => context.Name == rawName && context.IsNeonKube)); }
/// <summary> /// Returns the path to the kubecontext extension file path for a specific context /// by raw name. /// </summary> /// <param name="contextName">The kubecontext name.</param> /// <returns>The file path.</returns> public static string GetContextExtensionPath(KubeContextName contextName) { Covenant.Requires <ArgumentNullException>(contextName != null); // Kubecontext names may include a forward slash to specify a Kubernetes // namespace. This won't work for a file name, so we're going to replace // any of these with a "~". var rawName = (string)contextName; return(Path.Combine(ClustersFolder, $"{rawName.Replace("/", "~")}.context.yaml")); }
/// <summary> /// Removes a neonKUBE related kubecontext if it exists. /// </summary> /// <param name="name">The context name.</param> /// <param name="noSave">Optionally prevent context save after the change.</param> public void RemoveContext(KubeContextName name, bool noSave = false) { var context = GetContext(name); if (context != null) { RemoveContext(context); } else { NeonHelper.DeleteFile(KubeHelper.GetClusterLoginPath(name)); } }
/// <summary> /// Returns the kubecontext extension for the structured configuration name. /// </summary> /// <param name="name">The structured context name.</param> /// <returns>The <see cref="KubeContextExtension"/> or <c>null</c>.</returns> public static KubeContextExtension GetContextExtension(KubeContextName name) { Covenant.Requires <ArgumentNullException>(name != null); var path = GetContextExtensionPath(name); if (!File.Exists(path)) { return(null); } var extension = NeonHelper.YamlDeserialize <KubeContextExtension>(ReadFileTextWithRetry(path)); extension.SetPath(path); extension.ClusterDefinition?.Validate(); return(extension); }
/// <summary> /// Sets the current Kubernetes config context. /// </summary> /// <param name="contextName">The context name of <c>null</c> to clear the current context.</param> /// <exception cref="ArgumentException">Thrown if the context specified doesnt exist.</exception> public static void SetCurrentContext(KubeContextName contextName) { if (contextName == null) { cachedContext = null; Config.CurrentContext = null; } else { var newContext = Config.GetContext(contextName); if (newContext == null) { throw new ArgumentException($"Kubernetes [context={contextName}] does not exist."); } cachedContext = newContext; Config.CurrentContext = (string)contextName; } cachedClusterCertificate = null; Config.Save(); }
/// <summary> /// Parses a Kubernetes context name like: <b>USER</b> "@" <b>CLUSTER</b> [ "/" <b>NAMESPACE</b> ] /// </summary> /// <param name="text">The input text.</param> /// <returns>The parsed name.</returns> /// <remarks> /// <note> /// The username, cluster, and namespace will be converted to lowercase. /// </note> /// </remarks> /// <exception cref="FormatException">Thrown if the name is not valid.</exception> public static KubeContextName Parse(string text) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(text), nameof(text)); // $todo(jefflill): // // We should probably honor any Kubernetes restrictions on // the name parts. var pAt = text.IndexOf('@'); var pSlash = text.IndexOf('/'); if (pAt == -1) { throw new FormatException($"Kubernetes context [name={text}] is missing an '@'."); } if (pSlash != -1 && pSlash < pAt) { throw new FormatException($"Kubernetes context [name={text}] has a '/' before the '@'."); } var name = new KubeContextName(); name.User = text.Substring(0, pAt); pAt++; if (pSlash == -1) { name.Cluster = text.Substring(pAt); name.Namespace = "default"; } else { name.Cluster = text.Substring(pAt, pSlash - pAt); name.Namespace = text.Substring(pSlash + 1); if (name.Namespace == string.Empty) { name.Namespace = "default"; } } if (name.User == string.Empty) { throw new FormatException($"Kubernetes context [name={text}] specifies an invalid user."); } if (name.Cluster == string.Empty) { throw new FormatException($"Kubernetes context [name={text}] specifies an invalid cluster."); } name.User = name.User.ToLowerInvariant(); name.Cluster = name.Cluster.ToLowerInvariant(); name.Namespace = name.Namespace.ToLowerInvariant(); name.Validate(); return(name); }
/// <summary> /// Constructs the <see cref="ISetupController"/> to be used for setting up a cluster. /// </summary> /// <param name="clusterDefinition">The cluster definition.</param> /// <param name="maxParallel"> /// Optionally specifies the maximum number of node operations to be performed in parallel. /// This <b>defaults to 500</b> which is effectively infinite. /// </param> /// <param name="unredacted"> /// Optionally indicates that sensitive information <b>won't be redacted</b> from the setup logs /// (typically used when debugging). /// </param> /// <param name="debugMode">Optionally indicates that the cluster will be prepared in debug mode.</param> /// <param name="uploadCharts"> /// <para> /// Optionally specifies that the current Helm charts should be uploaded to replace the charts in the base image. /// </para> /// <note> /// This will be treated as <c>true</c> when <paramref name="debugMode"/> is passed as <c>true</c>. /// </note> /// </param> /// <param name = "clusterspace" > Optionally specifies the clusterspace for the operation.</param> /// <param name="neonCloudHeadendUri">Optionally overrides the neonCLOUD headend service URI. This defaults to <see cref="KubeConst.NeonCloudHeadendUri"/>.</param> /// <param name="disableConsoleOutput"> /// Optionally disables status output to the console. This is typically /// enabled for non-console applications. /// </param> /// <returns>The <see cref="ISetupController"/>.</returns> /// <exception cref="NeonKubeException">Thrown when there's a problem.</exception> public static ISetupController CreateClusterSetupController( ClusterDefinition clusterDefinition, int maxParallel = 500, bool unredacted = false, bool debugMode = false, bool uploadCharts = false, string clusterspace = null, string neonCloudHeadendUri = null, bool disableConsoleOutput = false) { Covenant.Requires <ArgumentNullException>(clusterDefinition != null, nameof(clusterDefinition)); Covenant.Requires <ArgumentException>(maxParallel > 0, nameof(maxParallel)); neonCloudHeadendUri ??= KubeConst.NeonCloudHeadendUri; clusterDefinition.Validate(); // Determine where the log files should go. var logFolder = KubeHelper.LogFolder; // Ensure that the [prepare-ok] file in the log folder exists, indicating that // the last prepare operation succeeded. var prepareOkPath = Path.Combine(logFolder, "prepare-ok"); if (!File.Exists(prepareOkPath)) { throw new NeonKubeException($"Cannot locate the [{prepareOkPath}] file. Cluster prepare must have failed."); } // Clear the log folder except for the [prepare-ok] file. if (Directory.Exists(logFolder)) { foreach (var file in Directory.GetFiles(logFolder, "*", SearchOption.TopDirectoryOnly)) { if (Path.GetFileName(file) != "prepare-ok") { NeonHelper.DeleteFile(file); } } } else { throw new DirectoryNotFoundException(logFolder); } // Reload the any KubeConfig file to ensure we're up-to-date. KubeHelper.LoadConfig(); // Do some quick checks to ensure that component versions look reasonable. //var kubernetesVersion = new Version(KubeVersions.Kubernetes); //var crioVersion = new Version(KubeVersions.Crio); //if (crioVersion.Major != kubernetesVersion.Major || crioVersion.Minor != kubernetesVersion.Minor) //{ // throw new NeonKubeException($"[{nameof(KubeConst)}.{nameof(KubeVersions.Crio)}={KubeVersions.Crio}] major and minor versions don't match [{nameof(KubeConst)}.{nameof(KubeVersions.Kubernetes)}={KubeVersions.Kubernetes}]."); //} // Initialize the cluster proxy. var contextName = KubeContextName.Parse($"root@{clusterDefinition.Name}"); var kubeContext = new KubeConfigContext(contextName); KubeHelper.InitContext(kubeContext); ClusterProxy cluster = null; cluster = new ClusterProxy( hostingManagerFactory: new HostingManagerFactory(() => HostingLoader.Initialize()), operation: ClusterProxy.Operation.Setup, clusterDefinition: clusterDefinition, nodeProxyCreator: (nodeName, nodeAddress) => { var logStream = new FileStream(Path.Combine(logFolder, $"{nodeName}.log"), FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); var logWriter = new StreamWriter(logStream); var context = KubeHelper.CurrentContext; var sshCredentials = context.Extension.SshCredentials ?? SshCredentials.FromUserPassword(KubeConst.SysAdminUser, KubeConst.SysAdminPassword); return(new NodeSshProxy <NodeDefinition>(nodeName, nodeAddress, sshCredentials, logWriter: logWriter)); }); if (unredacted) { cluster.SecureRunOptions = RunOptions.None; } // Configure the setup controller. var controller = new SetupController <NodeDefinition>($"Setup [{cluster.Definition.Name}] cluster", cluster.Nodes, KubeHelper.LogFolder, disableConsoleOutput: disableConsoleOutput) { MaxParallel = maxParallel, LogBeginMarker = "# CLUSTER-BEGIN-SETUP #########################################################", LogEndMarker = "# CLUSTER-END-SETUP-SUCCESS ###################################################", LogFailedMarker = "# CLUSTER-END-SETUP-FAILED ####################################################" }; // Load the cluster login information if it exists and when it indicates that // setup is still pending, we'll use that information (especially the generated // secure SSH password). // // Otherwise, we'll write (or overwrite) the context file with a fresh context. var clusterLoginPath = KubeHelper.GetClusterLoginPath((KubeContextName)$"{KubeConst.RootUser}@{clusterDefinition.Name}"); var clusterLogin = ClusterLogin.Load(clusterLoginPath); if (clusterLogin == null || !clusterLogin.SetupDetails.SetupPending) { clusterLogin = new ClusterLogin(clusterLoginPath) { ClusterDefinition = clusterDefinition, SshUsername = KubeConst.SysAdminUser, SetupDetails = new KubeSetupDetails() { SetupPending = true } }; clusterLogin.Save(); } // Update the cluster node SSH credentials to use the secure password. var sshCredentials = SshCredentials.FromUserPassword(KubeConst.SysAdminUser, clusterLogin.SshPassword); foreach (var node in cluster.Nodes) { node.UpdateCredentials(sshCredentials); } // Configure the setup controller state. controller.Add(KubeSetupProperty.Preparing, false); controller.Add(KubeSetupProperty.ReleaseMode, KubeHelper.IsRelease); controller.Add(KubeSetupProperty.DebugMode, debugMode); controller.Add(KubeSetupProperty.MaintainerMode, !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NC_ROOT"))); controller.Add(KubeSetupProperty.ClusterProxy, cluster); controller.Add(KubeSetupProperty.ClusterLogin, clusterLogin); controller.Add(KubeSetupProperty.HostingManager, cluster.HostingManager); controller.Add(KubeSetupProperty.HostingEnvironment, cluster.HostingManager.HostingEnvironment); controller.Add(KubeSetupProperty.ClusterspaceFolder, clusterspace); controller.Add(KubeSetupProperty.NeonCloudHeadendUri, neonCloudHeadendUri); controller.Add(KubeSetupProperty.Redact, !unredacted); // Configure the setup steps. controller.AddGlobalStep("resource requirements", KubeSetup.CalculateResourceRequirements); cluster.HostingManager.AddSetupSteps(controller); controller.AddWaitUntilOnlineStep("connect nodes"); controller.AddNodeStep("check node OS", (controller, node) => node.VerifyNodeOS()); controller.AddNodeStep("check image version", (controller, node) => { // Ensure that the node image version matches the current neonKUBE (build) version. var imageVersion = node.ImageVersion; if (imageVersion == null) { throw new Exception($"Node image is not stamped with the image version file: {KubeConst.ImageVersionPath}"); } if (imageVersion != SemanticVersion.Parse(KubeVersions.NeonKube)) { throw new Exception($"Node image version [{imageVersion}] does not match the neonKUBE version [{KubeVersions.NeonKube}] implemented by the current build."); } }); controller.AddNodeStep("disable cloud-init", (controller, node) => node.SudoCommand("touch /etc/cloud/cloud-init.disabled")); controller.AddNodeStep("node basics", (controller, node) => node.BaseInitialize(controller, upgradeLinux: false)); // $todo(jefflill): We don't support Linux distribution upgrades yet. controller.AddNodeStep("root certificates", (controller, node) => node.UpdateRootCertificates()); controller.AddNodeStep("setup ntp", (controller, node) => node.SetupConfigureNtp(controller)); controller.AddNodeStep("cluster metadata", ConfigureMetadataAsync); // Perform common configuration for the bootstrap node first. // We need to do this so the the package cache will be running // when the remaining nodes are configured. var configureControlPlaneStepLabel = cluster.Definition.ControlNodes.Count() > 1 ? "setup first control-plane node" : "setup control-plane node"; controller.AddNodeStep(configureControlPlaneStepLabel, (controller, node) => { node.SetupNode(controller, KubeSetup.ClusterManifest); }, (controller, node) => node == cluster.FirstControlNode); // Perform common configuration for the remaining nodes (if any). if (cluster.Definition.Nodes.Count() > 1) { controller.AddNodeStep("setup other nodes", (controller, node) => { node.SetupNode(controller, KubeSetup.ClusterManifest); node.InvokeIdempotent("setup/setup-node-restart", () => node.Reboot(wait: true)); }, (controller, node) => node != cluster.FirstControlNode); } if (debugMode) { controller.AddNodeStep("load images", (controller, node) => node.NodeLoadImagesAsync(controller, downloadParallel: 5, loadParallel: 3)); } controller.AddNodeStep("install helm", (controller, node) => { node.NodeInstallHelm(controller); }); controller.AddNodeStep("install kustomize", (controller, node) => { node.NodeInstallKustomize(controller); }); if (uploadCharts || debugMode) { controller.AddNodeStep("upload helm charts", (controller, node) => { cluster.FirstControlNode.SudoCommand($"rm -rf {KubeNodeFolder.Helm}/*"); cluster.FirstControlNode.NodeInstallHelmArchive(controller); var zipPath = LinuxPath.Combine(KubeNodeFolder.Helm, "charts.zip"); cluster.FirstControlNode.SudoCommand($"unzip {zipPath} -d {KubeNodeFolder.Helm}"); cluster.FirstControlNode.SudoCommand($"rm -f {zipPath}"); }, (controller, node) => node == cluster.FirstControlNode); } //----------------------------------------------------------------- // Cluster setup. controller.AddGlobalStep("setup cluster", controller => KubeSetup.SetupClusterAsync(controller)); controller.AddGlobalStep("persist state", controller => { // Indicate that setup is complete. clusterLogin.ClusterDefinition.ClearSetupState(); clusterLogin.SetupDetails.SetupPending = false; clusterLogin.Save(); }); //----------------------------------------------------------------- // Verify the cluster. controller.AddNodeStep("check control-plane nodes", (controller, node) => { KubeDiagnostics.CheckControlNode(node, cluster.Definition); }, (controller, node) => node.Metadata.IsControlPane); if (cluster.Workers.Count() > 0) { controller.AddNodeStep("check workers", (controller, node) => { KubeDiagnostics.CheckWorker(node, cluster.Definition); }, (controller, node) => node.Metadata.IsWorker); } cluster.HostingManager.AddPostSetupSteps(controller); // We need to dispose this after the setup controller runs. controller.AddDisposable(cluster); return(controller); }
/// <summary> /// Parses a Kubernetes context name. /// </summary> /// <param name="input">The input text.</param> /// <returns>The parsed name.</returns> /// <remarks> /// <para> /// neonKUBE supports a context name format that includes a user name like: /// </para> /// <example> /// <b>USER</b> "@" <b>CLUSTER</b> [ "/" <b>NAMESPACE</b> ] /// </example> /// <para> /// We assume that contexts including an <b>"@"</b> are associated with a /// neonKUBE cluster and use this to link to additional login information. /// We can also parse context names without an <b>"@"</b>. The parsed /// <see cref="User"/> property will be set to <c>null</c> in this case. /// </para> /// <example> /// <b>CLUSTER</b> [ "/" <b>NAMESPACE</b> ] /// </example> /// <note> /// The <see cref="User"/>, <see cref="Cluster"/>, and <see cref="Namespace"/> /// properties will be converted to lowercase. /// </note> /// </remarks> /// <exception cref="FormatException">Thrown if the name is not valid.</exception> public static KubeContextName Parse(string input) { if (string.IsNullOrEmpty(input)) { throw new FormatException("Context name cannot be empty."); } var pAt = input.IndexOf('@'); var pSlash = input.IndexOf('/'); if (pAt == -1) { // Looks like a standard (non-neonKUBE) context name. var name = new KubeContextName(); if (pSlash == -1) { name.Cluster = input; name.Namespace = "default"; } else { name.Cluster = input.Substring(0, pSlash); name.Namespace = input.Substring(pSlash + 1); if (name.Namespace == string.Empty) { name.Namespace = "default"; } } if (name.User == string.Empty) { throw new FormatException($"Kubernetes context [name={input}] specifies an invalid user."); } if (name.Cluster == string.Empty) { throw new FormatException($"Kubernetes context [name={input}] specifies an invalid cluster."); } name.User = null; name.Cluster = name.Cluster.ToLowerInvariant(); name.Namespace = name.Namespace.ToLowerInvariant(); name.Validate(); return(name); } else { // Looks like a neonKUBE context name. if (pSlash != -1 && pSlash < pAt) { throw new FormatException($"Kubernetes context [name={input}] has a '/' before the '@'."); } var name = new KubeContextName(); name.User = input.Substring(0, pAt); pAt++; if (pSlash == -1) { name.Cluster = input.Substring(pAt); name.Namespace = "default"; } else { name.Cluster = input.Substring(pAt, pSlash - pAt); name.Namespace = input.Substring(pSlash + 1); if (name.Namespace == string.Empty) { name.Namespace = "default"; } } if (name.User == string.Empty) { throw new FormatException($"Kubernetes context [name={input}] specifies an invalid user."); } if (name.Cluster == string.Empty) { throw new FormatException($"Kubernetes context [name={input}] specifies an invalid cluster."); } name.User = name.User.ToLowerInvariant(); name.Cluster = name.Cluster.ToLowerInvariant(); name.Namespace = name.Namespace.ToLowerInvariant(); name.Validate(); return(name); } }