public void SecretV1_Data_PreserveKeyCase(string key) { var model = new SecretV1 { Data = { [key] = key } }; JObject rootObject; using (JTokenWriter writer = new JTokenWriter()) { JsonSerializer.Create(KubeResourceClient.SerializerSettings).Serialize(writer, model); writer.Flush(); rootObject = (JObject)writer.Token; } Log.LogInformation("Serialized:\n{JSON:l}", rootObject.ToString(Formatting.Indented) ); JObject data = rootObject.Value <JObject>("data"); Assert.NotNull(data); Assert.Equal(key, data.Value <string>(key) ); }
public MetricsDeclarationBuilder WithStorageQueueMetric(string metricName = "promitor", string metricDescription = "Description for a metric", string queueName = "promitor-queue", string accountName = "promitor-account", string sasToken = "?sig=promitor", string azureMetricName = AzureStorageConstants.Queues.Metrics.MessageCount, string resourceDiscoveryGroupName = "", bool omitResource = false) { var secret = new SecretV1 { RawValue = sasToken }; var resource = new StorageQueueResourceV1 { QueueName = queueName, AccountName = accountName, SasToken = secret }; CreateAndAddMetricDefinition(ResourceType.StorageQueue, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, resource); return(this); }
public MetricsDeclarationBuilder WithAzureStorageQueueMetric(string metricName = "promitor", string metricDescription = "Description for a metric", string queueName = "promitor-queue", string accountName = "promitor-account", string sasToken = "?sig=promitor", string azureMetricName = AzureStorageConstants.Queues.Metrics.MessageCount) { var azureMetricConfiguration = CreateAzureMetricConfiguration(azureMetricName); var secret = new SecretV1 { RawValue = sasToken }; var resource = new StorageQueueResourceV1 { QueueName = queueName, AccountName = accountName, SasToken = secret }; var metric = new MetricDefinitionV1 { Name = metricName, Description = metricDescription, AzureMetricConfiguration = azureMetricConfiguration, Resources = new List <AzureResourceDefinitionV1> { resource }, ResourceType = ResourceType.StorageQueue }; _metrics.Add(metric); return(this); }
public ServiceResources(DeploymentV1 deployment, StatefulSetV1 statefulSet, ServiceV1 service, IngressV1Beta1 ingress, SecretV1 secret) { Deployment = deployment; StatefulSet = statefulSet; Service = service; Ingress = ingress; Secret = secret; }
/// <summary> /// Load configuration entries from the Secret. /// </summary> public override void Load() { SecretV1 secret = _client.SecretsV1().Get(_secretName, _kubeNamespace).GetAwaiter().GetResult(); if (secret != null) { string sectionNamePrefix = !String.IsNullOrWhiteSpace(_sectionName) ? _sectionName + ":" : String.Empty; Data = secret.Data.ToDictionary( entry => sectionNamePrefix + entry.Key.Replace('.', ':'), entry => { try { // Will choke on binary data that doesn't represent valid UTF8 text return(Encoding.UTF8.GetString( Convert.FromBase64String(entry.Value) )); } catch (FormatException) { // Not valid Base64; use raw value. return(entry.Value); } catch (ArgumentException) { // Not valid UTF8; use raw value. return(entry.Value); } }, StringComparer.OrdinalIgnoreCase ); } else { Data = new Dictionary <string, string>(); } if (_watch && _watchSubscription == null) { _watchSubscription = _client.SecretsV1() .Watch(_secretName, _kubeNamespace) .Subscribe(secretEvent => { if (secretEvent.EventType == ResourceEventType.Modified) { OnReload(); } }); } }
/// <summary> /// If the Secret <see cref="ResourceEventType"/> is Modified the internal Propertie will be reset in this Event. /// </summary> /// <param name="secretEvent"> /// Event Argument <see cref="IResourceEventV1"/> /// </param> private void OnKeyManagementSecretChanged(IResourceEventV1 <SecretV1> secretEvent) { if (secretEvent == null) { throw new ArgumentNullException(nameof(secretEvent)); } if (secretEvent.Resource == null) { throw new ArgumentNullException(nameof(secretEvent.Resource)); } if (secretEvent.EventType == ResourceEventType.Modified) { // Attach the changed Secret this._keyManagementSecret = secretEvent.Resource; } }
/// <summary> /// Update the specified Secret. /// </summary> /// <param name="client"> /// The <see cref="SecretV1"/> resource client. /// </param> /// <param name="secret"> /// A <see cref="SecretV1"/> representing the new state for the Secret. /// </param> /// <param name="cancellationToken"> /// An optional <see cref="CancellationToken"/> that can be used to cancel the operation. /// </param> /// <returns> /// A <see cref="SecretV1"/> representing the updated Secret. /// </returns> /// <remarks> /// Updates all mutable fields (if specified on <paramref name="secret"/>). /// </remarks> public static Task <SecretV1> Update(this SecretClientV1 client, SecretV1 secret, CancellationToken cancellationToken = default) { if (client == null) { throw new ArgumentNullException(nameof(client)); } if (String.IsNullOrWhiteSpace(secret?.Metadata?.Name)) { throw new ArgumentException("Cannot update a Secret if its metadata does not specify a name.", nameof(secret)); } if (String.IsNullOrWhiteSpace(secret?.Metadata?.Namespace)) { throw new ArgumentException("Cannot update a Secret if its metadata does not specify a namespace.", nameof(secret)); } return(client.Update( name: secret.Metadata.Name, kubeNamespace: secret.Metadata.Namespace, patchAction: patch => { if (secret.Metadata.Labels != null) { patch.Replace(patchSecret => patchSecret.Metadata.Labels, value: secret.Metadata.Labels ); } if (secret.Metadata.Annotations != null) { patch.Replace(patchSecret => patchSecret.Metadata.Annotations, value: secret.Metadata.Annotations ); } if (secret.Data != null) { patch.Replace(patchSecret => patchSecret.Data, value: secret.Data ); } }, cancellationToken: cancellationToken )); }
/// <summary> /// Request creation of a <see cref="Secret"/>. /// </summary> /// <param name="newSecret"> /// A <see cref="SecretV1"/> representing the Secret to create. /// </param> /// <param name="cancellationToken"> /// An optional <see cref="CancellationToken"/> that can be used to cancel the request. /// </param> /// <returns> /// A <see cref="SecretV1"/> representing the current state for the newly-created Secret. /// </returns> public async Task <SecretV1> Create(SecretV1 newSecret, CancellationToken cancellationToken = default) { if (newSecret == null) { throw new ArgumentNullException(nameof(newSecret)); } return(await Http .PostAsJsonAsync( Requests.Collection.WithTemplateParameters(new { Namespace = newSecret?.Metadata?.Namespace ?? Client.DefaultNamespace }), postBody : newSecret, cancellationToken : cancellationToken ) .ReadContentAsAsync <SecretV1, StatusV1>()); }
private SecretV1 CreateNewKubeSecretModel(ISecret secret) { var kubeSecret = new SecretV1() { Metadata = new ObjectMetaV1() { Name = secret.Name, Namespace = secret.Namespace } }; foreach (var label in _configuration.Labels) { kubeSecret.Metadata.Labels.Add(label.Key, label.Value); } return(kubeSecret); }
public void Deserialize_SasTokenSupplied_UsesDeserializer() { // Arrange const string yamlText = @"sasToken: rawValue: abc123"; var node = YamlUtils.CreateYamlNode(yamlText); var sasTokenNode = (YamlMappingNode)node.Children["sasToken"]; var secret = new SecretV1(); _secretDeserializer.Setup(d => d.DeserializeObject(sasTokenNode, _errorReporter.Object)).Returns(secret); // Act var resource = _deserializer.Deserialize(node, _errorReporter.Object); // Assert Assert.Same(secret, resource.SasToken); }
/// <summary> /// Load or Create the Kubernetes Secret /// </summary> private void LoadOrCreateSecret() { // Try to get the Secret SecretV1 secret = this._client.SecretsV1().Get(_secretName, _kubeNamespace).GetAwaiter().GetResult(); if (secret == null) { // Create a new Secret secret = this._client.SecretsV1().Create(new SecretV1() { Metadata = new ObjectMetaV1() { Name = _secretName, Namespace = _kubeNamespace } }).GetAwaiter().GetResult(); } // Use the Secret this._keyManagementSecret = secret; }
/// <summary> /// Load configuration entries from the Secret. /// </summary> public override void Load() { Log.LogTrace("Attempting to load Secret {SecretName} in namespace {KubeNamespace}...", _secretName, _kubeNamespace ?? _client.DefaultNamespace); SecretV1 secret = _client.SecretsV1().Get(_secretName, _kubeNamespace).GetAwaiter().GetResult(); Load(secret); if (_watch && _watchSubscription == null) { Log.LogTrace("Creating watch-event stream for Secret {SecretName} in namespace {KubeNamespace}...", _secretName, _kubeNamespace ?? _client.DefaultNamespace); _watchSubscription = _client.SecretsV1() .Watch(_secretName, _kubeNamespace) .Subscribe(OnSecretChanged); Log.LogTrace("Watch-event stream created for Secret {SecretName} in namespace {KubeNamespace}.", _secretName, _kubeNamespace ?? _client.DefaultNamespace); } }
/// <summary> /// Ensure that a Secret for credentials does not exist for the specified database server. /// </summary> /// <returns> /// <c>true</c>, if the controller is now absent; otherwise, <c>false</c>. /// </returns> public async Task <bool> EnsureCredentialsSecretAbsent() { RequireCurrentState(); SecretV1 credentialsSecret = await FindCredentialsSecret(); if (credentialsSecret == null) { return(true); } Log.LogInformation("Deleting credentials secret {SecretName} for server {ServerId}...", credentialsSecret.Metadata.Name, State.Id ); try { await KubeClient.SecretsV1().Delete( name: credentialsSecret.Metadata.Name, kubeNamespace: KubeOptions.KubeNamespace ); } catch (HttpRequestException <StatusV1> deleteFailed) { Log.LogError("Failed to delete credentials secret {SecretName} for server {ServerId} (Message:{FailureMessage}, Reason:{FailureReason}).", credentialsSecret.Metadata.Name, State.Id, deleteFailed.Response.Message, deleteFailed.Response.Reason ); return(false); } Log.LogInformation("Deleted credentials secret {SecretName} for server {ServerId}.", credentialsSecret.Metadata.Name, State.Id ); return(true); }
/// <summary> /// Load data from the specified Secret. /// </summary> /// <param name="secret"> /// A <see cref="SecretV1"/> representing the Secret's current state, or <c>null</c> if the Secret was not found. /// </param> void Load(SecretV1 secret) { if (secret != null) { Log.LogTrace("Found Secret {SecretName} in namespace {KubeNamespace}.", _secretName, _kubeNamespace ?? _client.DefaultNamespace); string sectionNamePrefix = !String.IsNullOrWhiteSpace(_sectionName) ? _sectionName + ":" : String.Empty; Data = secret.Data.ToDictionary( entry => sectionNamePrefix + entry.Key.Replace('.', ':'), entry => { try { // Will choke on binary data that doesn't represent valid UTF8 text return(Encoding.UTF8.GetString( Convert.FromBase64String(entry.Value) )); } catch (FormatException) { // Not valid Base64; use raw value. return(entry.Value); } catch (ArgumentException) { // Not valid UTF8; use raw value. return(entry.Value); } }, StringComparer.OrdinalIgnoreCase ); } else { Log.LogTrace("Secret {SecretName} was not found in namespace {KubeNamespace}.", _secretName, _kubeNamespace ?? _client.DefaultNamespace); Data = new Dictionary <string, string>(); } }
/// <summary> /// Request creation of a <see cref="SecretV1"/>. /// </summary> /// <param name="newSecret"> /// A <see cref="SecretV1"/> representing the Secret to create. /// </param> /// <param name="cancellationToken"> /// An optional <see cref="CancellationToken"/> that can be used to cancel the request. /// </param> /// <returns> /// A <see cref="SecretV1"/> representing the current state for the newly-created Secret. /// </returns> public async Task <SecretV1> Create(SecretV1 newSecret, CancellationToken cancellationToken = default) { if (newSecret == null) { throw new ArgumentNullException(nameof(newSecret)); } return(await Http .PostAsJsonAsync( Requests.Collection.WithTemplateParameters(new { Namespace = newSecret?.Metadata?.Namespace ?? KubeClient.DefaultNamespace }), postBody : newSecret, cancellationToken : cancellationToken ) .ReadContentAsObjectV1Async <SecretV1>( operationDescription: $"create v1/Secret resource in namespace '{newSecret?.Metadata?.Namespace ?? KubeClient.DefaultNamespace}'" )); }
/// <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); }
private async Task <ServiceResources> DeployService(DeployCommand.Types.Service command, string kubeNamespace) { DeploymentV1 deployment = null; StatefulSetV1 statefulSet = null; var secret = await CreateSecret(); if (string.IsNullOrWhiteSpace(command.PersistentStoragePath)) { deployment = await CreateDeployment(); } else { statefulSet = await CreateStatefulSet(); } var service = await CreateService(); var ingress = await CreateServiceIngress(); return(new ServiceResources(deployment, statefulSet, service, ingress, secret)); async Task <SecretV1> CreateSecret() { if (!command.Secrets.Any()) { return(null); } var existingSecret = await kubeApiClient.SecretsV1().Get(command.Name, kubeNamespace); var secretResource = new SecretV1 { Metadata = new ObjectMetaV1 { Name = command.Name, Namespace = kubeNamespace, }, Type = "Opaque", }; if (existingSecret != null) { foreach (var existingData in existingSecret.Data) { secretResource.Data[existingData.Key] = existingData.Value; } } foreach (var secretCommand in command.Secrets.Where(s => s.Value != null)) { secretResource.Data[secretCommand.Name] = Convert.ToBase64String(Encoding.UTF8.GetBytes(secretCommand.Value)); } foreach (var existingSecretNames in secretResource.Data.Keys) { if (command.Secrets.All(secretCommand => secretCommand.Name != existingSecretNames)) { secretResource.Data.Remove(existingSecretNames); } } return(await kubeApiClient.Dynamic().Apply(secretResource, fieldManager: "clud", force: true)); } async Task <DeploymentV1> CreateDeployment() { var deployment = new DeploymentV1 { Metadata = new ObjectMetaV1 { Name = command.Name, Namespace = kubeNamespace, }, Spec = new DeploymentSpecV1 { Selector = new LabelSelectorV1 { MatchLabels = { { KubeNaming.AppLabelKey, command.Name } }, }, Replicas = command.Replicas, Template = new PodTemplateSpecV1 { Metadata = new ObjectMetaV1 { Name = command.Name, Namespace = kubeNamespace, Labels = { { KubeNaming.AppLabelKey, command.Name } } }, Spec = new PodSpecV1 { Containers = { new ContainerV1 { Name = command.Name, Image = DockerImageName(), } }, }, } } }; AddEnvironmentVariables(deployment.Spec.Template.Spec.Containers.Single().Env); return(await kubeApiClient.Dynamic().Apply(deployment, fieldManager: "clud", force: true)); } async Task <StatefulSetV1> CreateStatefulSet() { var statefulSet = new StatefulSetV1 { Metadata = new ObjectMetaV1 { Name = command.Name, Namespace = kubeNamespace, }, Spec = new StatefulSetSpecV1 { Selector = new LabelSelectorV1 { MatchLabels = { { KubeNaming.AppLabelKey, command.Name } }, }, Template = new PodTemplateSpecV1 { Metadata = new ObjectMetaV1 { Name = command.Name, Namespace = kubeNamespace, Labels = { { KubeNaming.AppLabelKey, command.Name } } }, Spec = new PodSpecV1 { Containers = { new ContainerV1 { Name = command.Name, Image = DockerImageName(), VolumeMounts = { new VolumeMountV1 { Name = command.Name, MountPath = command.PersistentStoragePath, } } }, }, }, }, VolumeClaimTemplates = { new PersistentVolumeClaimV1 { Metadata = new ObjectMetaV1 { Name = command.Name, Namespace = kubeNamespace, }, Spec = new PersistentVolumeClaimSpecV1 { AccessModes ={ "ReadWriteOnce" }, Resources = new ResourceRequirementsV1 { Requests ={ { "storage", "100Mi" } }, } } } } } }; AddEnvironmentVariables(statefulSet.Spec.Template.Spec.Containers.Single().Env); return(await kubeApiClient.Dynamic().Apply(statefulSet, fieldManager: "clud", force: true)); } string DockerImageName() { return(command.IsPublicDockerImage ? command.DockerImage : $"{KubeNaming.DockerRegistryLocation}/{command.DockerImage}"); } void AddEnvironmentVariables(List <EnvVarV1> envVarV1s) { envVarV1s.AddRange(command.EnvironmentVariables.Select(env => new EnvVarV1 { Name = env.Name, Value = env.Value })); envVarV1s.AddRange(command.Secrets.Select(secret => new EnvVarV1 { Name = secret.Name, ValueFrom = new EnvVarSourceV1 { SecretKeyRef = new SecretKeySelectorV1 { Name = command.Name, Key = secret.Name, Optional = false, } } })); } async Task <ServiceV1> CreateService() { var service = new ServiceV1 { Metadata = new ObjectMetaV1 { Name = command.Name, Namespace = kubeNamespace, }, Spec = new ServiceSpecV1 { Selector = { { KubeNaming.AppLabelKey, command.Name } }, }, }; if (command.HttpPort != null) { service.Spec.Ports.Add(new ServicePortV1 { Name = KubeNaming.HttpPortName, Protocol = "TCP", Port = command.HttpPort.Value }); } service.Spec.Ports.AddRange(command.TcpPorts.Select(port => new ServicePortV1 { Name = $"tcp-{port}", Protocol = "TCP", Port = port, })); service.Spec.Ports.AddRange(command.UdpPorts.Select(port => new ServicePortV1 { Name = $"udp-{port}", Protocol = "UDP", Port = port, })); return(await kubeApiClient.Dynamic().Apply(service, fieldManager: "clud", force: true)); } async Task <IngressV1Beta1> CreateServiceIngress() { if (command.HttpPort == null) { return(null); } var ingress = new IngressV1Beta1 { Metadata = new ObjectMetaV1 { Name = command.Name, Namespace = kubeNamespace, }, Spec = new IngressSpecV1Beta1 { Rules = { new IngressRuleV1Beta1 { Host = $"{command.Name}-{kubeNamespace}.{cludOptions.BaseHostname}", Http = new HTTPIngressRuleValueV1Beta1 { Paths = { new HTTPIngressPathV1Beta1 { Path = "/", Backend = new IngressBackendV1Beta1 { ServiceName = service.Metadata.Name, ServicePort = KubeNaming.HttpPortName, } } } } } }, }, }; return(await kubeApiClient.Dynamic().Apply(ingress, fieldManager: "clud", force: true)); } }
/// <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(); } }