/// <summary> /// Create a new <see cref="DatabaseServerProvisioner"/>. /// </summary> /// <param name="logger"> /// The provisioner's logger. /// </param> /// <param name="kubeClient"> /// The <see cref="KubeApiClient"/> used to communicate with the Kubernetes API. /// </param> /// <param name="kubeOptions"> /// Application-level Kubernetes settings. /// </param> /// <param name="kubeResources"> /// A factory for Kubernetes resource models. /// </param> /// <param name="databaseProxyClient"> /// The <see cref="DatabaseProxyApiClient"/> used to communicate with the Database Proxy API. /// </param> public DatabaseServerProvisioner(ILogger <DatabaseServerProvisioner> logger, KubeApiClient kubeClient, DatabaseProxyApiClient databaseProxyClient, IOptions <KubernetesOptions> kubeOptions, KubeResources kubeResources) : base(logger) { if (logger == null) { throw new ArgumentNullException(nameof(logger)); } if (kubeClient == null) { throw new ArgumentNullException(nameof(kubeClient)); } if (databaseProxyClient == null) { throw new ArgumentNullException(nameof(databaseProxyClient)); } if (kubeOptions == null) { throw new ArgumentNullException(nameof(kubeOptions)); } if (kubeResources == null) { throw new ArgumentNullException(nameof(kubeResources)); } KubeClient = kubeClient; DatabaseProxyClient = databaseProxyClient; KubeOptions = kubeOptions.Value; KubeResources = kubeResources; }
/// <summary> /// Create a <see cref="SecretV1"/> for deploying a Glider Gun Remote node. /// </summary> /// <param name="resources"> /// The Kubernetes resource template service. /// </param> /// <param name="options"> /// The current options for the deployment tool. /// </param> /// <returns> /// The configured <see cref="SecretV1"/>. /// </returns> public static SecretV1 DeployGliderGunRemoteSecret(this KubeResources resources, ProgramOptions options) { if (resources == null) { throw new ArgumentNullException(nameof(resources)); } if (options == null) { throw new ArgumentNullException(nameof(options)); } return(resources.OpaqueSecret( name: resources.Names.DeployGliderGunRemoteSecret(options), kubeNamespace: options.KubeNamespace, labels: new Dictionary <string, string> { ["glider-gun.job.name"] = options.JobName, ["glider-gun.job.type"] = "deploy.glider-gun.remote" }, data: new Dictionary <string, string> { ["id_rsa"] = Convert.ToBase64String( File.ReadAllBytes(options.SshPrivateKeyFile) ), ["id_rsa.pub"] = Convert.ToBase64String( File.ReadAllBytes(options.SshPublicKeyFile ?? options.SshPrivateKeyFile + ".pub") ) } )); }
/// <summary> /// Create a <see cref="JobV1"/> for deploying a Glider Gun Remote node. /// </summary> /// <param name="resources"> /// The Kubernetes resource template service. /// </param> /// <param name="options"> /// The current options for the deployment tool. /// </param> /// <returns> /// The configured <see cref="JobV1"/>. /// </returns> public static JobV1 DeployGliderGunRemoteJob(this KubeResources resources, ProgramOptions options) { if (resources == null) { throw new ArgumentNullException(nameof(resources)); } if (options == null) { throw new ArgumentNullException(nameof(options)); } return(resources.Job( name: resources.Names.DeployGliderGunRemoteJob(options), kubeNamespace: options.KubeNamespace, spec: resources.Specs.DeployGliderGunRemoteJob(options), labels: new Dictionary <string, string> { ["glider-gun.job.name"] = options.JobName, ["glider-gun.job.type"] = "deploy.glider-gun.remote" } )); }
/// <summary> /// Ensure that a Secret for data exists for the specified database server. /// </summary> /// <returns> /// The Secret resource, as a <see cref="SecretV1"/>. /// </returns> public async Task <SecretV1> EnsureCredentialsSecretPresent() { RequireCurrentState(); SecretV1 existingSecret = await FindCredentialsSecret(); if (existingSecret != null) { Log.LogInformation("Found existing credentials secret {SecretName} for server {ServerId}.", existingSecret.Metadata.Name, State.Id ); return(existingSecret); } Log.LogInformation("Creating credentials secret for server {ServerId}...", State.Id ); Log.LogInformation("Requesting X.509 certificate..."); CertificateCredentials serverCertificate = await RequestServerCertificate(); SecretV1 createdSecret = await KubeClient.SecretsV1().Create( KubeResources.CredentialsSecret(State, serverCertificate, kubeNamespace: KubeOptions.KubeNamespace ) ); Log.LogInformation("Successfully created credentials secret {SecretName} for server {ServerId}.", createdSecret.Metadata.Name, State.Id ); return(createdSecret); }
/// <summary> /// Create a new <see cref="ServerCredentialsProvisioner"/>. /// </summary> /// <param name="logger"> /// The provisioner's logger. /// </param> /// <param name="kubeClient"> /// The <see cref="KubeApiClient"/> used to communicate with the Kubernetes API. /// </param> /// <param name="vaultClient"> /// The <see cref="IVaultClient"/> used to communicate with the Vault API. /// </param> /// <param name="kubeResources"> /// A factory for Kubernetes resource models. /// </param> /// <param name="vaultOptions"> /// Application-level Vault settings. /// </param> /// <param name="kubeOptions"> /// Application-level Kubernetes settings. /// </param> public ServerCredentialsProvisioner(ILogger <DatabaseServerProvisioner> logger, KubeApiClient kubeClient, IVaultClient vaultClient, KubeResources kubeResources, IOptions <VaultOptions> vaultOptions, IOptions <KubernetesOptions> kubeOptions) : base(logger) { if (kubeClient == null) { throw new ArgumentNullException(nameof(kubeClient)); } if (vaultClient == null) { throw new ArgumentNullException(nameof(vaultClient)); } if (kubeResources == null) { throw new ArgumentNullException(nameof(kubeResources)); } if (vaultOptions == null) { throw new ArgumentNullException(nameof(vaultOptions)); } if (kubeOptions == null) { throw new ArgumentNullException(nameof(kubeOptions)); } KubeClient = kubeClient; VaultClient = vaultClient; KubeResources = kubeResources; VaultOptions = vaultOptions.Value; KubeOptions = kubeOptions.Value; }
/// <summary> /// The main program entry-point. /// </summary> /// <param name="commandLineArguments"> /// The program's command-line arguments. /// </param> /// <returns> /// The program exit-code. /// </returns> static async Task <int> Main(string[] commandLineArguments) { // Show help if no arguments are specified. bool showHelp = commandLineArguments.Length == 0; if (showHelp) { commandLineArguments = new[] { "--help" } } ; try { SynchronizationContext.SetSynchronizationContext( new SynchronizationContext() ); ProgramOptions options = ProgramOptions.Parse(commandLineArguments); if (options == null) { return(showHelp ? ExitCodes.Success : ExitCodes.InvalidArguments); } ConfigureLogging(options); using (ServiceProvider serviceProvider = BuildServiceProvider(options)) using (AutoResetEvent done = new AutoResetEvent(initialState: false)) { KubeApiClient client = serviceProvider.GetRequiredService <KubeApiClient>(); KubeResources kubeResources = serviceProvider.GetRequiredService <KubeResources>(); string jobName = kubeResources.Names.DeployGliderGunRemoteJob(options); JobV1 existingJob = await client.JobsV1().Get(jobName); if (existingJob != null) { Log.Information("Found existing job {JobName} in namespace {KubeNamespace}; deleting...", existingJob.Metadata.Name, existingJob.Metadata.Namespace ); await client.JobsV1().Delete(jobName, propagationPolicy: DeletePropagationPolicy.Foreground ); Log.Information("Deleted existing job {JobName}.", existingJob.Metadata.Name, existingJob.Metadata.Namespace ); } string secretName = kubeResources.Names.DeployGliderGunRemoteSecret(options); SecretV1 existingSecret = await client.SecretsV1().Get(secretName); if (existingSecret != null) { Log.Information("Found existing secret {SecretName} in namespace {KubeNamespace}; deleting...", existingSecret.Metadata.Name, existingSecret.Metadata.Namespace ); await client.SecretsV1().Delete(secretName); Log.Information("Deleted existing secret {SecretName}.", existingSecret.Metadata.Name, existingSecret.Metadata.Namespace ); } Log.Information("Creating deployment secret {SecretName}...", secretName); SecretV1 deploymentSecret = kubeResources.DeployGliderGunRemoteSecret(options); try { deploymentSecret = await client.SecretsV1().Create(deploymentSecret); } catch (HttpRequestException <StatusV1> createSecretFailed) { Log.Error(createSecretFailed, "Failed to create Kubernetes Secret {SecretName} for deployment ({Reason}): {ErrorMessage}", secretName, createSecretFailed.Response.Reason, createSecretFailed.Response.Message ); return(ExitCodes.JobFailed); } Log.Information("Created deployment secret {SecretName}.", deploymentSecret.Metadata.Name); // Watch for job's associated pod to start, then monitor the pod's log until it completes. IDisposable jobLogWatch = null; IDisposable jobPodWatch = client.PodsV1().WatchAll( labelSelector: $"job-name={jobName}", kubeNamespace: options.KubeNamespace ).Subscribe( podEvent => { if (jobLogWatch != null) { return; } PodV1 jobPod = podEvent.Resource; if (jobPod.Status.Phase != "Pending") { Log.Information("Job {JobName} has started.", jobName); Log.Verbose("Hook up log monitor for Pod {PodName} of Job {JobName}...", jobPod.Metadata.Name, jobName ); jobLogWatch = client.PodsV1().StreamLogs( name: jobPod.Metadata.Name, kubeNamespace: jobPod.Metadata.Namespace ).Subscribe( logEntry => { Log.Information("[{PodName}] {LogEntry}", jobPod.Metadata.Name, logEntry); }, error => { if (error is HttpRequestException <StatusV1> requestError) { Log.Error(requestError, "Kubernetes API request error ({Reason}): {ErrorMessage:l}", requestError.Response.Reason, requestError.Response.Message ); } else { Log.Error(error, "JobLog Error"); } }, () => { Log.Information("[{PodName}] <end of log>", jobPod.Metadata.Name); done.Set(); } ); Log.Information("Monitoring log for Pod {PodName} of Job {JobName}.", jobPod.Metadata.Name, jobName ); } }, error => { Log.Error(error, "PodWatch Error"); }, () => { Log.Information("PodWatch End"); } ); Log.Information("Creating deployment job {JobName}...", jobName); JobV1 deploymentJob = kubeResources.DeployGliderGunRemoteJob(options); try { deploymentJob = await client.JobsV1().Create(deploymentJob); } catch (HttpRequestException <StatusV1> createJobFailed) { Log.Error(createJobFailed, "Failed to create Kubernetes Job {JobName} for deployment ({Reason}): {ErrorMessage}", jobName, createJobFailed.Response.Reason, createJobFailed.Response.Message ); return(ExitCodes.JobFailed); } Log.Information("Created deployment job {JobName}.", deploymentJob.Metadata.Name); TimeSpan timeout = TimeSpan.FromSeconds(options.Timeout); Log.Information("Waiting up to {TimeoutSeconds} seconds for deployment job {JobName} to complete.", timeout.TotalSeconds, jobName ); if (!done.WaitOne(timeout)) { using (jobPodWatch) using (jobLogWatch) { Log.Error("Timed out after waiting {TimeoutSeconds} seconds for deployment job {JobName} to complete.", timeout.TotalSeconds, jobName ); return(ExitCodes.JobTimeout); } } jobPodWatch?.Dispose(); jobLogWatch?.Dispose(); deploymentJob = await client.JobsV1().Get(jobName); if (deploymentJob == null) { Log.Error("Cannot find deployment job {JobName} in namespace {KubeNamespace}.", deploymentJob.Metadata.Name, deploymentJob.Metadata.Namespace ); return(ExitCodes.UnexpectedError); } if (deploymentJob.Status.Failed > 0) { Log.Error("Deployment job {JobName} failed.", deploymentJob.Metadata.Name ); foreach (JobConditionV1 jobCondition in deploymentJob.Status.Conditions) { Log.Error("Deployment job {JobName} failed ({Reason}): {ErrorMessage}.", deploymentJob.Metadata.Name, jobCondition.Reason, jobCondition.Message ); } return(ExitCodes.JobFailed); } if (deploymentJob.Status.Succeeded > 0) { Log.Information("Deployment job {JobName} completed successfully.", deploymentJob.Metadata.Name ); } } Log.Information("Done."); return(ExitCodes.Success); } catch (HttpRequestException <StatusV1> kubeRequestError) { Log.Error(kubeRequestError, "A Kubernetes API request failed while deploying the remote node ({Reason}): {ErrorMessage}", kubeRequestError.Response.Reason, kubeRequestError.Response.Message ); return(ExitCodes.JobFailed); } catch (Exception unexpectedError) { Log.Error(unexpectedError, "An unexpected error occurred while deploying the remote node."); return(ExitCodes.UnexpectedError); } finally { Log.CloseAndFlush(); } }