コード例 #1
0
        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)
                         );
        }
コード例 #2
0
        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);
        }
コード例 #3
0
        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);
        }
コード例 #4
0
ファイル: DeploymentsService.cs プロジェクト: CharlesRea/clud
 public ServiceResources(DeploymentV1 deployment, StatefulSetV1 statefulSet, ServiceV1 service, IngressV1Beta1 ingress, SecretV1 secret)
 {
     Deployment  = deployment;
     StatefulSet = statefulSet;
     Service     = service;
     Ingress     = ingress;
     Secret      = secret;
 }
コード例 #5
0
        /// <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;
            }
        }
コード例 #7
0
        /// <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
                       ));
        }
コード例 #8
0
        /// <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>());
        }
コード例 #9
0
ファイル: SecretsSinker.cs プロジェクト: linjmeyer/sinker
        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);
        }
コード例 #10
0
        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;
        }
コード例 #12
0
        /// <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);
            }
        }
コード例 #13
0
        /// <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);
        }
コード例 #14
0
        /// <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>();
            }
        }
コード例 #15
0
        /// <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}'"
                       ));
        }
コード例 #16
0
        /// <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);
        }
コード例 #17
0
ファイル: DeploymentsService.cs プロジェクト: CharlesRea/clud
        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));
            }
        }
コード例 #18
0
ファイル: Program.cs プロジェクト: lulzzz/glider-gun-v2
        /// <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();
            }
        }