diff --git a/src/Services/ScheduleProcessor.cs b/src/Services/ScheduleProcessor.cs index fb6e32c..136d8c2 100644 --- a/src/Services/ScheduleProcessor.cs +++ b/src/Services/ScheduleProcessor.cs @@ -227,7 +227,10 @@ private void WaitForFileToFinishCopying(string filePath) { || _response.DeployRequest.DeployAction == MessageFormats.PlatformServices.Deployment.DeployRequest.Types.DeployActions.Create || _response.DeployRequest.DeployAction == MessageFormats.PlatformServices.Deployment.DeployRequest.Types.DeployActions.Delete) { WaitForFileToFinishCopying(Path.Combine(_scheduleImportDirectory, _response.DeployRequest.YamlFileContents)); + + string yamlFilePath = Path.Combine(_scheduleImportDirectory, _response.DeployRequest.YamlFileContents); _response.DeployRequest.YamlFileContents = File.ReadAllText(Path.Combine(_scheduleImportDirectory, _response.DeployRequest.YamlFileContents)); + File.Delete(yamlFilePath); } @@ -473,24 +476,53 @@ private MessageFormats.PlatformServices.Deployment.DeployResponse ValidatePrereq if (request.DeployAction == MessageFormats.PlatformServices.Deployment.DeployRequest.Types.DeployActions.Apply || request.DeployAction == MessageFormats.PlatformServices.Deployment.DeployRequest.Types.DeployActions.Create || request.DeployAction == MessageFormats.PlatformServices.Deployment.DeployRequest.Types.DeployActions.Delete) { - if (string.IsNullOrWhiteSpace(request.YamlFileContents)) + + if (string.IsNullOrWhiteSpace(request.YamlFileContents)) { errorFields.Add(nameof(request.YamlFileContents)); + } else { + // This lets the schedule retry next go round + if (!File.Exists(Path.Combine(_scheduleImportDirectory, request.YamlFileContents))) { + throw new FileNotFoundException($"YamlFile '{request.YamlFileContents}' does not exist at {_scheduleImportDirectory}.", fileName: request.YamlFileContents); + } + + // Wait for the file to finish copying or timeout if we didn't receive one + try { + WaitForFileToFinishCopying(Path.Combine(_scheduleImportDirectory, request.YamlFileContents)); + } catch (TimeoutException) { + // Only trip an error if the file is required + if (request.AppContextFile.Required == true) { + throw new FileNotFoundException($"YamlFile '{request.YamlFileContents}' still copying to {_scheduleImportDirectory}.", fileName: request.YamlFileContents); + } + } + } + System.Text.RegularExpressions.Regex regex = new System.Text.RegularExpressions.Regex("[^a-zA-Z0-9_-]"); if (regex.IsMatch(Path.GetFileNameWithoutExtension(request.YamlFileContents))) { response.ResponseHeader.Message += "YamlFileContents value invalid (special characters founds). (RegEx matched on '[^a-zA-Z0-9_]')."; errorFields.Add(nameof(request.YamlFileContents)); } - - if (!File.Exists(Path.Combine(_scheduleImportDirectory, request.YamlFileContents))) { - response.ResponseHeader.Message += $"YamlFile '{request.YamlFileContents}' does not exist at {_scheduleImportDirectory}."; - errorFields.Add(nameof(request.YamlFileContents)); - } } if (request.AppContainerImage != null) { - if (string.IsNullOrWhiteSpace(request.AppContainerImage.TarballFileName)) + if (string.IsNullOrWhiteSpace(request.AppContainerImage.TarballFileName)) { errorFields.Add(nameof(request.AppContainerImage.TarballFileName)); + } else { + // This lets the schedule retry next go round + if (!File.Exists(Path.Combine(_scheduleImportDirectory, request.AppContainerImage.TarballFileName))) { + throw new FileNotFoundException($"AppContainerImage Tarball '{request.AppContainerImage.TarballFileName}' does not exist at {_scheduleImportDirectory}.", fileName: request.AppContainerImage.TarballFileName); + } + + // Wait for the file to finish copying or timeout if we didn't receive one + try { + WaitForFileToFinishCopying(Path.Combine(_scheduleImportDirectory, request.AppContainerImage.TarballFileName)); + } catch (TimeoutException) { + // Only trip an error if the file is required + if (request.AppContextFile.Required == true) { + throw new FileNotFoundException($"DockerFile '{request.AppContainerImage.TarballFileName}' still copying to {_scheduleImportDirectory}.", fileName: request.AppContainerImage.TarballFileName); + } + } + } if (string.IsNullOrWhiteSpace(request.AppContainerImage.DestinationRepository)) errorFields.Add(nameof(request.AppContainerImage.DestinationRepository)); @@ -500,8 +532,24 @@ private MessageFormats.PlatformServices.Deployment.DeployResponse ValidatePrereq } if (request.AppContainerBuild != null) { - if (string.IsNullOrWhiteSpace(request.AppContainerBuild.DockerFile)) + if (string.IsNullOrWhiteSpace(request.AppContainerBuild.DockerFile)) { errorFields.Add(nameof(request.AppContainerBuild.DockerFile)); + } else { + // This lets the schedule retry next go round + if (!File.Exists(Path.Combine(_scheduleImportDirectory, request.AppContainerBuild.DockerFile))) { + throw new FileNotFoundException($"DockerFile'{request.AppContainerBuild.DockerFile}' does not exist at {_scheduleImportDirectory}.", fileName: request.AppContainerBuild.DockerFile); + } + + // Wait for the file to finish copying or timeout if we didn't receive one + try { + WaitForFileToFinishCopying(Path.Combine(_scheduleImportDirectory, request.AppContainerBuild.DockerFile)); + } catch (TimeoutException) { + // Only trip an error if the file is required + if (request.AppContextFile.Required == true) { + throw new FileNotFoundException($"DockerFile '{request.AppContainerBuild.DockerFile}' still copying to {_scheduleImportDirectory}.", fileName: request.AppContainerBuild.DockerFile); + } + } + } if (string.IsNullOrWhiteSpace(request.AppContainerBuild.DestinationRepository)) errorFields.Add(nameof(request.AppContainerBuild.DestinationRepository)); @@ -514,14 +562,18 @@ private MessageFormats.PlatformServices.Deployment.DeployResponse ValidatePrereq if (string.IsNullOrWhiteSpace(request.AppContextFile.FileName)) { errorFields.Add(nameof(request.AppContextFile.FileName)); } else { + // This lets the schedule retry next go round + if (!File.Exists(Path.Combine(_scheduleImportDirectory, request.AppContextFile.FileName))) { + throw new FileNotFoundException($"AppContextFile '{request.AppContextFile.FileName}' does not exist at {_scheduleImportDirectory}.", fileName: request.AppContextFile.FileName); + } + // Wait for the file to finish copying or timeout if we didn't receive one try { WaitForFileToFinishCopying(Path.Combine(_scheduleImportDirectory, request.AppContextFile.FileName)); } catch (TimeoutException) { // Only trip an error if the file is required if (request.AppContextFile.Required == true) { - response.ResponseHeader.Message += $"AppContextFile '{request.AppContextFile.FileName}' is still being copied. Please wait for the file to finish copying."; - errorFields.Add(nameof(request.AppContextFile.FileName)); + throw new FileNotFoundException($"AppContextFile '{request.AppContextFile.FileName}' still copying to {_scheduleImportDirectory}.", fileName: request.AppContextFile.FileName); } } } diff --git a/src/Utils/K8sClient.cs b/src/Utils/K8sClient.cs index 75d76f0..60162c7 100644 --- a/src/Utils/K8sClient.cs +++ b/src/Utils/K8sClient.cs @@ -198,6 +198,9 @@ private void PatchViaYamlObject(IKubernetesObject yamlObject) { case V1PersistentVolume pv: _k8sClient.PatchPersistentVolume(new V1Patch(pv, V1Patch.PatchType.MergePatch), name: pv.Metadata.Name); break; + case V1Secret secret: + _k8sClient.PatchNamespacedSecret(new V1Patch(secret, V1Patch.PatchType.MergePatch), name: secret.Metadata.Name, namespaceParameter: secret.Metadata.NamespaceProperty); + break; case V1ConfigMap cm: _k8sClient.PatchNamespacedConfigMap(new V1Patch(cm, V1Patch.PatchType.MergePatch), name: cm.Metadata.Name, namespaceParameter: cm.Metadata.NamespaceProperty); break; @@ -230,6 +233,8 @@ private void PatchViaYamlObject(IKubernetesObject yamlObject) { if (job.Metadata == null || string.IsNullOrEmpty(job.Metadata.NamespaceProperty)) { throw new NullReferenceException("Metadata.NamespaceProperty is null or empty"); } _k8sClient.PatchNamespacedJob(new V1Patch(job, V1Patch.PatchType.MergePatch), name: job.Metadata.Name, namespaceParameter: job.Metadata.NamespaceProperty); break; + default: + throw new Exception(string.Format($"Unknown object type: {yamlObject.GetType()}")); } } catch (k8s.Autorest.HttpOperationException ex) { if (ex.Response.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) { @@ -259,6 +264,9 @@ private void DeleteViaYamlObject(IKubernetesObject yamlObject) { case V1PersistentVolume pv: _k8sClient.DeletePersistentVolume(name: pv.Metadata.Name); break; + case V1Secret secret: + _k8sClient.DeleteNamespacedSecret(name: secret.Metadata.Name, namespaceParameter: secret.Metadata.NamespaceProperty); + break; case V1ConfigMap cm: if (cm.Metadata == null || string.IsNullOrEmpty(cm.Metadata.NamespaceProperty)) { throw new NullReferenceException("Metadata.NamespaceProperty is null or empty"); } _k8sClient.DeleteNamespacedConfigMap(name: cm.Metadata.Name, namespaceParameter: cm.Metadata.NamespaceProperty); @@ -292,6 +300,8 @@ private void DeleteViaYamlObject(IKubernetesObject yamlObject) { if (job.Metadata == null || string.IsNullOrEmpty(job.Metadata.NamespaceProperty)) { throw new NullReferenceException("Metadata.NamespaceProperty is null or empty"); } _k8sClient.DeleteNamespacedJob(name: job.Metadata.Name, namespaceParameter: job.Metadata.NamespaceProperty, propagationPolicy: "Background"); break; + default: + throw new Exception(string.Format($"Unknown object type: {yamlObject.GetType()}")); } } catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound) { // We can ignore the error if we're trying to delete something and it's not found. @@ -312,6 +322,10 @@ private void CreateViaYamlObject(IKubernetesObject yamlObject) { case V1Namespace ns: _k8sClient.CreateNamespace(body: ns); break; + case V1Secret secret: + if (secret.Metadata == null || string.IsNullOrEmpty(secret.Metadata.NamespaceProperty)) { throw new NullReferenceException("Metadata.NamespaceProperty is null or empty"); } + _k8sClient.CreateNamespacedSecret(body: secret, namespaceParameter: secret.Metadata.NamespaceProperty); + break; case V1ConfigMap cm: if (cm.Metadata == null || string.IsNullOrEmpty(cm.Metadata.NamespaceProperty)) { throw new NullReferenceException("Metadata.NamespaceProperty is null or empty"); } _k8sClient.CreateNamespacedConfigMap(body: cm, namespaceParameter: cm.Metadata.NamespaceProperty); @@ -342,6 +356,8 @@ private void CreateViaYamlObject(IKubernetesObject yamlObject) { if (job.Metadata == null || string.IsNullOrEmpty(job.Metadata.NamespaceProperty)) { throw new NullReferenceException("Metadata.NamespaceProperty is null or empty"); } _k8sClient.CreateNamespacedJob(body: job, namespaceParameter: job.Metadata.NamespaceProperty); break; + default: + throw new Exception(string.Format($"Unknown object type: {yamlObject.GetType()}")); } } catch (Exception ex) { _logger.LogError("Failed to create object of type '{yamlKind}'. Error: {error}", yamlObject.Kind, ex.Message); diff --git a/src/Utils/TemplateUtil.cs b/src/Utils/TemplateUtil.cs index bbc901d..bf45bfd 100644 --- a/src/Utils/TemplateUtil.cs +++ b/src/Utils/TemplateUtil.cs @@ -39,7 +39,8 @@ public TemplateUtil(ILogger logger, IServiceProvider serviceProvid internal List GenerateKubernetesObjectsFromDeployment(MessageFormats.PlatformServices.Deployment.DeployResponse deploymentItem) { List returnList = new() { GenerateAppSettings(deploymentItem), - GenerateServiceAccount(deploymentItem) + GenerateServiceAccount(deploymentItem), + GenerateSecrets(deploymentItem) }; GeneratePersistentVolumes(deploymentItem).ForEach(pv => returnList.Add(pv)); @@ -351,6 +352,35 @@ public V1ConfigMap GenerateAppSettings(MessageFormats.PlatformServices.Deploymen return returnValue; } + public V1Secret GenerateSecrets(MessageFormats.PlatformServices.Deployment.DeployResponse deploymentItem) { + _logger.LogDebug("Generating secrets template. (AppName: '{AppName}' / trackingId: '{trackingId}' / correlationId: '{correlationId}')'", + deploymentItem.DeployRequest.AppName, + deploymentItem.ResponseHeader.TrackingId, + deploymentItem.ResponseHeader.CorrelationId); + + Dictionary templateRequest = StandardTemplateRequestItems(deploymentItem); + templateRequest.Add("services.payloadapp.payloadappTemplate.secrets.enabled", "true"); + + string templateYaml = GenerateTemplate(templateRequest); + + V1Secret? returnValue = KubernetesYaml.LoadAllFromString(templateYaml) + .OfType() + .FirstOrDefault(); + + if (returnValue == null) + throw new ApplicationException("Failed to generate SpaceFX Secrets"); + + // The template returns the values in base64 encoded strings. We have to decode them to their byte values. + // Kubernetes normally does this for us on yaml ingession, but we're skipping that and add items via the API + // So we just have to convert the arrays to strings, base64 decode them, then convert them back to byte arrays + foreach (var kvp in returnValue.Data) { + string decodedValue = Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(kvp.Value))); + returnValue.Data[kvp.Key] = Encoding.UTF8.GetBytes(decodedValue); + } + + return returnValue; + } + public List GenerateVolumes(MessageFormats.PlatformServices.Deployment.DeployResponse deploymentItem) { _logger.LogDebug("Generating volumes template. (AppName: '{AppName}' / trackingId: '{trackingId}' / correlationId: '{correlationId}')'", deploymentItem.DeployRequest.AppName,