/// <summary> /// Constructs a cluster proxy from a cluster login. /// </summary> /// <param name="kubeContext">The cluster context.</param> /// <param name="nodeProxyCreator"> /// The optional application supplied function that creates a node proxy /// given the node name, public address or FQDN, private address, and /// the node definition. /// </param> /// <param name="appendToLog">Optionally have logs appended to an existing log file rather than creating a new one.</param> /// <param name="defaultRunOptions"> /// Optionally specifies the <see cref="RunOptions"/> to be assigned to the /// <see cref="SshProxy{TMetadata}.DefaultRunOptions"/> property for the /// nodes managed by the cluster proxy. This defaults to <see cref="RunOptions.None"/>. /// </param> /// <remarks> /// The <paramref name="nodeProxyCreator"/> function will be called for each node in /// the cluster definition giving the application the chance to create the management /// proxy using the node's SSH credentials and also to specify logging. A default /// creator that doesn't initialize SSH credentials and logging is used if <c>null</c> /// is passed. /// </remarks> public ClusterProxy( KubeConfigContext kubeContext, NodeProxyCreator nodeProxyCreator = null, bool appendToLog = false, RunOptions defaultRunOptions = RunOptions.None) : this(kubeContext.Extension.ClusterDefinition, nodeProxyCreator, appendToLog : appendToLog, defaultRunOptions : defaultRunOptions) { Covenant.Requires <ArgumentNullException>(kubeContext != null); this.KubeContext = kubeContext; }
/// <summary> /// Clears all cached items. /// </summary> private static void ClearCachedItems() { cachedConfig = null; cachedContext = null; cachedHeadendClient = null; cachedNeonKubeUserFolder = null; cachedKubeUserFolder = null; cachedRunFolder = null; cachedLogFolder = null; cachedClustersFolder = null; cachedPasswordsFolder = null; cachedCacheFolder = null; cachedDesktopFolder = null; cachedClientConfig = null; cachedClusterCertificate = null; cachedProgramFolder = null; cachedPwshPath = null; }
/// <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> /// This is used for special situations for setting up a cluster to /// set an uninitialized Kubernetes config context as the current /// <see cref="CurrentContext"/>. /// </summary> /// <param name="context">The context being set or <c>null</c> to reset.</param> public static void InitContext(KubeConfigContext context = null) { cachedContext = context; }
/// <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> /// Removes a neonKUBE related kubecontext if it exists. /// </summary> /// <param name="context">The context to be removed.</param> /// <param name="noSave">Optionally prevent context save after the change.</param> public void RemoveContext(KubeConfigContext context, bool noSave = false) { Covenant.Requires <ArgumentNullException>(context != null, nameof(context)); for (int i = 0; i < Contexts.Count; i++) { if (Contexts[i].Name == context.Name) { Contexts.RemoveAt(i); break; } } // Remove the referenced cluster and user if they're not // referenced by another context (to prevent orphans). for (int i = 0; i < Clusters.Count; i++) { if (Clusters[i].Name == context.Properties.Cluster) { Clusters.RemoveAt(i); break; } } for (int i = 0; i < Users.Count; i++) { if (Users[i].Name == context.Properties.User) { Users.RemoveAt(i); break; } } // Clear the current context if the removed context was the current one. if (CurrentContext == context.Name) { CurrentContext = null; } // Persist as required. if (!noSave) { Save(); // We need to remove the extension file too (if one exists). var extensionPath = Path.Combine(KubeHelper.LoginsFolder, $"{context.Name}.login.yaml"); try { File.Delete(extensionPath); } catch (IOException) { // Intentially ignoring this. } } }
/// <summary> /// Adds or updates a kubecontext. /// </summary> /// <param name="context">The new context.</param> /// <param name="cluster">The context cluster information.</param> /// <param name="user">The context user information.</param> /// <param name="noSave">Optionally prevent context save after the change.</param> public void SetContext(KubeConfigContext context, KubeConfigCluster cluster, KubeConfigUser user, bool noSave = false) { Covenant.Requires <ArgumentNullException>(context != null, nameof(context)); Covenant.Requires <ArgumentNullException>(cluster != null, nameof(cluster)); Covenant.Requires <ArgumentNullException>(user != null, nameof(user)); Covenant.Requires <ArgumentNullException>(context.Properties.Cluster == cluster.Name, nameof(context)); Covenant.Requires <ArgumentNullException>(context.Properties.User == user.Name, nameof(context)); var updated = false; for (int i = 0; i < Contexts.Count; i++) { if (Contexts[i].Name == context.Name) { Contexts[i] = context; updated = true; break; } } if (!updated) { Contexts.Add(context); } // We also need to add or update the referenced cluster and user properties. updated = false; for (int i = 0; i < Clusters.Count; i++) { if (Clusters[i].Name == context.Properties.Cluster) { Clusters[i] = cluster; updated = true; break; } } if (!updated) { Clusters.Add(cluster); } updated = false; for (int i = 0; i < Users.Count; i++) { if (Users[i].Name == context.Properties.User) { Users[i] = user; updated = true; break; } } if (!updated) { Users.Add(user); } // Persist as required. if (!noSave) { Save(); } }