Esempio n. 1
0
        /// <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();
        }
Esempio n. 2
0
        /// <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));
        }
Esempio n. 3
0
        /// <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"));
        }
Esempio n. 4
0
        /// <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));
            }
        }
Esempio n. 5
0
        /// <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);
        }
Esempio n. 6
0
        /// <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();
        }
Esempio n. 7
0
        /// <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);
        }
Esempio n. 8
0
        /// <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);
        }
Esempio n. 9
0
        /// <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);
            }
        }