/// <summary> /// Request creation of a <see cref="ReplicaSetV1"/>. /// </summary> /// <param name="newReplicaSet"> /// A <see cref="ReplicaSetV1"/> representing the ReplicaSet to create. /// </param> /// <param name="cancellationToken"> /// An optional <see cref="CancellationToken"/> that can be used to cancel the request. /// </param> /// <returns> /// A <see cref="ReplicaSetV1"/> representing the current state for the newly-created ReplicaSet. /// </returns> public async Task <ReplicaSetV1> Create(ReplicaSetV1 newReplicaSet, CancellationToken cancellationToken = default) { if (newReplicaSet == null) { throw new ArgumentNullException(nameof(newReplicaSet)); } return(await Http .PostAsJsonAsync( Requests.Collection.WithTemplateParameters(new { Namespace = newReplicaSet?.Metadata?.Namespace ?? KubeClient.DefaultNamespace }), postBody : newReplicaSet, cancellationToken : cancellationToken ) .ReadContentAsObjectV1Async <ReplicaSetV1>( operationDescription: $"create v1/ReplicaSet in namespace '{newReplicaSet?.Metadata?.Namespace ?? KubeClient.DefaultNamespace}'" )); }
/// <summary> /// Find the ReplicaSet that corresponds to the specified revision of the specified Deployment. /// </summary> /// <param name="client"> /// The Kubernetes API client. /// </param> /// <param name="deployment"> /// The target Deployment. /// </param> /// <param name="targetRevision"> /// The target revision. /// </param> /// <param name="cancellationToken"> /// An optional <see cref="CancellationToken"/> that can be used to cancel the request. /// </param> /// <returns> /// A <see cref="ReplicaSetV1"/> representing the ReplicaSet's current state; <c>null</c>, if no corresponding ReplicaSet was found. /// </returns> public static async Task <ReplicaSetV1> FindReplicaSetForRevision(IKubeApiClient client, DeploymentV1 deployment, int targetRevision, CancellationToken cancellationToken = default) { if (client == null) { throw new ArgumentNullException(nameof(client)); } if (deployment == null) { throw new ArgumentNullException(nameof(deployment)); } string matchLabelSelector = deployment.GetLabelSelector(); ReplicaSetListV1 replicaSets = await client.ReplicaSetsV1().List(matchLabelSelector, deployment.Metadata.Namespace, cancellationToken); ReplicaSetV1 targetRevisionReplicaSet = replicaSets.Items.FirstOrDefault( replicaSet => replicaSet.GetRevision() == targetRevision ); return(targetRevisionReplicaSet); }
/// <summary> /// Determine whether a Deployment owns a ReplicaSet. /// </summary> /// <param name="deployment"> /// The Deployment to examine. /// </param> /// <param name="replicaSet"> /// The ReplicaSet to examine. /// </param> /// <returns> /// <c>true</c>, if the ReplicaSet has an owner-reference to the Deployment; otherwise, <c>false</c>. /// </returns> static bool DoesDeploymentOwnReplicaSet(DeploymentV1 deployment, ReplicaSetV1 replicaSet) { if (deployment == null) { throw new ArgumentNullException(nameof(deployment)); } if (replicaSet == null) { throw new ArgumentNullException(nameof(replicaSet)); } // Sanity-check: does the target ReplicaSet actually represent a revision of the existing Deployment? bool isReplicaSetForDeployment = replicaSet.Metadata.OwnerReferences.Any(ownerReference => ownerReference.Kind == deployment.Kind && ownerReference.ApiVersion == deployment.ApiVersion && ownerReference.Name == deployment.Metadata.Name ); return(isReplicaSetForDeployment); }
/// <summary> /// Find the ReplicaSet that corresponds to the specified revision of the specified Deployment. /// </summary> /// <param name="client"> /// The Kubernetes API client. /// </param> /// <param name="deployment"> /// The target Deployment. /// </param> /// <param name="targetRevision"> /// The target revision. /// </param> /// <returns> /// A <see cref="ReplicaSetV1"/> representing the ReplicaSet's current state; <c>null</c>, if no corresponding ReplicaSet was found. /// </returns> static async Task <ReplicaSetV1> FindReplicaSetForRevision(IKubeApiClient client, DeploymentV1 deployment, int targetRevision) { if (client == null) { throw new ArgumentNullException(nameof(client)); } if (deployment == null) { throw new ArgumentNullException(nameof(deployment)); } ReplicaSetListV1 replicaSets = await client.ReplicaSetsV1().List( labelSelector: $"app={deployment.Metadata.Name}", kubeNamespace: deployment.Metadata.Namespace ); ReplicaSetV1 targetRevisionReplicaSet = replicaSets.Items.FirstOrDefault( replicaSet => replicaSet.GetRevision() == targetRevision ); return(targetRevisionReplicaSet); }
/// <summary> /// Roll back a Deployment to the revision represented by the specified ReplicaSet. /// </summary> /// <param name="client"> /// The Kubernetes API client. /// </param> /// <param name="existingDeployment"> /// The target Deployment. /// </param> /// <param name="targetRevisionReplicaSet"> /// The ReplicaSet that represents the target revision. /// </param> /// <returns> /// A <see cref="DeploymentV1"/> representing the Deployment's current state. /// </returns> static async Task <DeploymentV1> RollbackDeployment(IKubeApiClient client, DeploymentV1 existingDeployment, ReplicaSetV1 targetRevisionReplicaSet) { if (client == null) { throw new ArgumentNullException(nameof(client)); } if (existingDeployment == null) { throw new ArgumentNullException(nameof(existingDeployment)); } if (targetRevisionReplicaSet == null) { throw new ArgumentNullException(nameof(targetRevisionReplicaSet)); } if (!DoesDeploymentOwnReplicaSet(existingDeployment, targetRevisionReplicaSet)) { throw new InvalidOperationException($"ReplicaSet '{targetRevisionReplicaSet.Metadata.Name}' in namespace '{targetRevisionReplicaSet.Metadata.Namespace}' is not owned by Deployment '{existingDeployment.Metadata.Name}'."); } int?targetRevision = targetRevisionReplicaSet.GetRevision(); if (targetRevision == null) { throw new InvalidOperationException($"Cannot determine Deployment revision represented by ReplicaSet '{targetRevisionReplicaSet.Metadata.Name}' in namespace '{targetRevisionReplicaSet.Metadata.Namespace}'."); } DeploymentV1 rolledBackDeployment = await client.DeploymentsV1().Update(existingDeployment.Metadata.Name, kubeNamespace: existingDeployment.Metadata.Namespace, patchAction: patch => { patch.Replace(deployment => deployment.Spec.Template.Spec, value: targetRevisionReplicaSet.Spec.Template.Spec ); // Since the Rollback API is now obsolete, we have to update the Deployment's revision by hand. Dictionary <string, string> annotationsWithModifiedRevision = existingDeployment.Metadata.Annotations; annotationsWithModifiedRevision[K8sAnnotations.Deployment.Revision] = targetRevision.Value.ToString(); patch.Replace(deployment => deployment.Metadata.Annotations, value: annotationsWithModifiedRevision ); }); // Re-fetch Deployment state so we pick up annotations added or updated by the controller. rolledBackDeployment = await client.DeploymentsV1().Get(rolledBackDeployment.Metadata.Name, rolledBackDeployment.Metadata.Namespace); return(rolledBackDeployment); }
/// <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) { ProgramOptions options = ProgramOptions.Parse(commandLineArguments); if (options == null) { return(ExitCodes.InvalidArguments); } ILoggerFactory loggerFactory = ConfigureLogging(options); try { KubeClientOptions clientOptions = K8sConfig.Load().ToKubeClientOptions(defaultKubeNamespace: options.KubeNamespace); if (options.Verbose) { clientOptions.LogPayloads = true; } KubeApiClient client = KubeApiClient.Create(clientOptions, loggerFactory); Log.Information("Looking for existing Deployment {DeploymentName} in namespace {KubeNamespace}...", options.DeploymentName, options.KubeNamespace ); DeploymentV1 existingDeployment = await client.DeploymentsV1().Get(options.DeploymentName, options.KubeNamespace); if (existingDeployment != null) { Log.Error("Cannot continue - deployment {DeploymentName} in namespace {KubeNamespace} already exists.", options.DeploymentName, options.KubeNamespace ); return(ExitCodes.AlreadyExists); } Log.Information("Ok, Deployment does not exist yet - we're ready to go."); Log.Information("Creating Deployment {DeploymentName} in namespace {KubeNamespace}...", options.DeploymentName, options.KubeNamespace ); DeploymentV1 initialDeployment = await CreateInitialDeployment(client, options.DeploymentName, options.KubeNamespace); int?initialRevision = initialDeployment.GetRevision(); if (initialRevision == null) { Log.Error("Unable to determine initial revision of Deployment {DeploymentName} in namespace {KubeNamespace} (missing annotation).", options.DeploymentName, options.KubeNamespace ); return(ExitCodes.UnexpectedError); } Log.Information("Created Deployment {DeploymentName} in namespace {KubeNamespace} (revision {DeploymentRevision}).", options.DeploymentName, options.KubeNamespace, initialRevision ); Log.Information("Updating Deployment {DeploymentName} in namespace {KubeNamespace}...", options.DeploymentName, options.KubeNamespace ); DeploymentV1 updatedDeployment = await UpdateDeployment(client, initialDeployment); int?updatedRevision = updatedDeployment.GetRevision(); if (updatedRevision == null) { Log.Error("Unable to determine updated revision of Deployment {DeploymentName} in namespace {KubeNamespace} (missing annotation).", options.DeploymentName, options.KubeNamespace ); return(ExitCodes.UnexpectedError); } Log.Information("Updated Deployment {DeploymentName} in namespace {KubeNamespace} (revision {DeploymentRevision}).", options.DeploymentName, options.KubeNamespace, updatedRevision ); Log.Information("Searching for ReplicaSet that corresponds to revision {Revision} of {DeploymentName} in namespace {KubeNamespace}...", options.DeploymentName, options.KubeNamespace, initialRevision ); ReplicaSetV1 targetReplicaSet = await FindReplicaSetForRevision(client, updatedDeployment, initialRevision.Value); if (targetReplicaSet == null) { Log.Error("Cannot find ReplicaSet that corresponds to revision {Revision} of {DeploymentName} in namespace {KubeNamespace}...", options.DeploymentName, options.KubeNamespace, initialRevision ); return(ExitCodes.NotFound); } Log.Information("Found ReplicaSet {ReplicaSetName} in namespace {KubeNamespace}.", targetReplicaSet.Metadata.Name, targetReplicaSet.Metadata.Namespace ); Log.Information("Rolling Deployment {DeploymentName} in namespace {KubeNamespace} back to initial revision {DeploymentRevision}...", options.DeploymentName, options.KubeNamespace, initialRevision ); DeploymentV1 rolledBackDeployment = await RollbackDeployment(client, updatedDeployment, targetReplicaSet); Log.Information("Rollback initiated for Deployment {DeploymentName} in namespace {KubeNamespace} from revision {FromRevision} to {ToRevision} (new revision will be {NewRevision})...", options.DeploymentName, options.KubeNamespace, updatedRevision, initialRevision, rolledBackDeployment.GetRevision() ); return(ExitCodes.Success); } catch (HttpRequestException <StatusV1> kubeError) { Log.Error(kubeError, "Kubernetes API error: {@Status}", kubeError.Response); return(ExitCodes.UnexpectedError); } catch (Exception unexpectedError) { Log.Error(unexpectedError, "Unexpected error."); return(ExitCodes.UnexpectedError); } }
/// <summary> /// Roll back a Deployment to the revision represented by the specified ReplicaSet. /// </summary> /// <param name="client"> /// The Kubernetes API client. /// </param> /// <param name="existingDeployment"> /// The target Deployment. /// </param> /// <param name="targetRevisionReplicaSet"> /// The ReplicaSet that represents the target revision. /// </param> /// <param name="cancellationToken"> /// An optional <see cref="CancellationToken"/> that can be used to cancel the request. /// </param> /// <returns> /// A <see cref="DeploymentV1"/> representing the Deployment's current state. /// </returns> public static async Task <DeploymentV1> RollbackDeployment(IKubeApiClient client, DeploymentV1 existingDeployment, ReplicaSetV1 targetRevisionReplicaSet, CancellationToken cancellationToken = default) { if (client == null) { throw new ArgumentNullException(nameof(client)); } if (existingDeployment == null) { throw new ArgumentNullException(nameof(existingDeployment)); } if (targetRevisionReplicaSet == null) { throw new ArgumentNullException(nameof(targetRevisionReplicaSet)); } int?targetRevision = targetRevisionReplicaSet.GetRevision(); if (targetRevision == null) { throw new InvalidOperationException($"Cannot determine Deployment revision represented by ReplicaSet '{targetRevisionReplicaSet.Metadata.Name}' in namespace '{targetRevisionReplicaSet.Metadata.Namespace}'."); } DeploymentV1 rolledBackDeployment = await client.DeploymentsV1().Update(existingDeployment.Metadata.Name, kubeNamespace: existingDeployment.Metadata.Namespace, cancellationToken: cancellationToken, patchAction: patch => { // Restore Deployment's Pod-template specification to the one used by the target ReplicaSet. patch.Replace(deployment => deployment.Spec.Template.Spec, value: targetRevisionReplicaSet.Spec.Template.Spec ); // Since the old Rollback API is obsolete (as of v1beta2), we have to update the Deployment's revision by hand. patch.Replace(deployment => deployment.Metadata.Annotations, // Due to JSON-PATCH limitations in the K8s API, we have to replace the entire Annotations property, not attempt to update individual items within the dictionary. value: new Dictionary <string, string>(existingDeployment.Metadata.Annotations) { [K8sAnnotations.Deployment.Revision] = targetRevision.Value.ToString() } ); }); // Re-fetch Deployment state so we pick up annotations added or updated by the controller. rolledBackDeployment = await client.DeploymentsV1().Get(rolledBackDeployment.Metadata.Name, rolledBackDeployment.Metadata.Namespace, cancellationToken); return(rolledBackDeployment); }