/// <summary> /// Perform a JSON patch operation on a Kubernetes resource. /// </summary> /// <typeparam name="TResource"> /// The target resource type. /// </typeparam> /// <param name="patchAction"> /// A delegate that performs customisation of the patch operation. /// </param> /// <param name="request"> /// An <see cref="HttpRequest"/> representing the patch request. /// </param> /// <param name="cancellationToken"> /// An optional <see cref="CancellationToken"/> that can be used to cancel the request. /// </param> /// <returns> /// A <typeparamref name="TResource"/> representing the updated resource. /// </returns> protected async Task <TResource> PatchResourceRaw <TResource>(Action <JsonPatchDocument> patchAction, HttpRequest request, CancellationToken cancellationToken) where TResource : KubeResourceV1 { if (patchAction == null) { throw new ArgumentNullException(nameof(patchAction)); } if (request == null) { throw new ArgumentNullException(nameof(request)); } // If possible, tell the consumer which resource type we had a problem with (helpful when all you find is the error message in the log). (string kind, string apiVersion) = KubeObjectV1.GetKubeKind <TResource>(); var patch = new JsonPatchDocument(); patchAction(patch); return(await Http.PatchAsync(request, patchBody : patch, mediaType : PatchMediaType, cancellationToken : cancellationToken ) .ReadContentAsObjectV1Async <TResource>( operationDescription: $"patch {apiVersion}/{kind} resource" ) .ConfigureAwait(false)); }
/// <summary> /// Request deletion of the specified global (non-namespaced) resource. /// </summary> /// <typeparam name="TResource"> /// The type of resource to delete. /// </typeparam> /// <param name="resourceByNameNoNamespaceRequestTemplate"> /// The HTTP request template for addressing a non-namespaced <typeparamref name="TResource"/> by name. /// </param> /// <param name="name"> /// The name of the resource to delete. /// </param> /// <param name="propagationPolicy"> /// A <see cref="DeletePropagationPolicy"/> indicating how child resources should be deleted (if at all). /// </param> /// <param name="cancellationToken"> /// An optional <see cref="CancellationToken"/> that can be used to cancel the request. /// </param> /// <returns> /// A <typeparamref name="TResource"/> representing the resource's most recent state before it was deleted, if <paramref name="propagationPolicy"/> is <see cref="DeletePropagationPolicy.Foreground"/>; otherwise, a <see cref="StatusV1"/> indicating the operation result. /// </returns> protected async Task <KubeResourceResultV1 <TResource> > DeleteGlobalResource <TResource>(HttpRequest resourceByNameNoNamespaceRequestTemplate, string name, DeletePropagationPolicy?propagationPolicy = null, CancellationToken cancellationToken = default) where TResource : KubeResourceV1 { if (resourceByNameNoNamespaceRequestTemplate == null) { throw new ArgumentNullException(nameof(resourceByNameNoNamespaceRequestTemplate)); } if (String.IsNullOrWhiteSpace(name)) { throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'name'.", nameof(name)); } var response = Http.DeleteAsJsonAsync( resourceByNameNoNamespaceRequestTemplate.WithTemplateParameters(new { Name = name }), deleteBody: new DeleteOptionsV1 { PropagationPolicy = propagationPolicy }, cancellationToken: cancellationToken ); (string kind, string apiVersion) = KubeObjectV1.GetKubeKind <TResource>(); string operationDescription = $"delete {apiVersion}/{kind} resource '{name}'"; return(await response.ReadContentAsResourceOrStatusV1 <TResource>(operationDescription, HttpStatusCode.OK, HttpStatusCode.NotFound)); }
public void KubeObjectV1_Kind_Success(Type kubeObjectType, string expectedKind, string expectedApiVersion) { KubeObjectV1 kubeObject = (KubeObjectV1)Activator.CreateInstance(kubeObjectType); Assert.Equal(expectedKind, kubeObject.Kind); Assert.Equal(expectedApiVersion, kubeObject.ApiVersion); }
/// <summary> /// Retrieve metadata for a Kubernetes resource API. /// </summary> /// <param name="modelType"> /// The CLR <see cref="Type"/> of the model that represents the resource. /// </param> /// <returns> /// The API metadata, or <c>null</c> if no metadata was found for the API. /// </returns> public KubeApiMetadata Get(Type modelType) { if (modelType == null) { throw new ArgumentNullException(nameof(modelType)); } (string kind, string apiVersion) = KubeObjectV1.GetKubeKind(modelType); if (String.IsNullOrWhiteSpace(kind)) { throw new ArgumentException($"Model type {modelType.FullName} has not been decorated with KubeResourceAttribute or KubeResourceListAttribute.", nameof(modelType)); } return(Get(kind, apiVersion)); }
public void KubeObject_JTokenType(Type kubeObjectType, JTokenType expectedTokenType) { KubeObjectV1 kubeObject = (KubeObjectV1)Activator.CreateInstance(kubeObjectType); JToken rootToken; using (JTokenWriter writer = new JTokenWriter()) { new JsonSerializer().Serialize(writer, kubeObject); writer.Flush(); rootToken = writer.Token; } Assert.NotNull(rootToken); Assert.Equal(rootToken.Type, expectedTokenType); }
/// <summary> /// Read response content as either a <see cref="StatusV1"/> or a <typeparamref name="TResource"/> resource.<see cref="KubeObjectV1"/>. /// </summary> /// <typeparam name="TResource"> /// The expected resource type. /// </typeparam> /// <param name="response"> /// The HTTP response. /// </param> /// <param name="operationDescription"> /// A short description of the operation represented by the request (used in exception message if request was not successful). /// </param> /// <param name="successStatusCodes"> /// Optional <see cref="HttpStatusCode"/>s that should be treated as representing a successful response. /// </param> /// <returns> /// The response content, as a <see cref="KubeObjectV1"/>. /// </returns> /// <exception cref="HttpRequestException{TResponse}"> /// The response status code was unexpected or did not represent success. /// </exception> /// <exception cref="InvalidOperationException"> /// No formatters were configured for the request, or an appropriate formatter could not be found in the request's list of formatters. /// </exception> public static async Task <KubeResourceResultV1 <TResource> > ReadContentAsResourceOrStatusV1 <TResource>(this Task <HttpResponseMessage> response, string operationDescription, params HttpStatusCode[] successStatusCodes) where TResource : KubeResourceV1 { if (response == null) { throw new ArgumentNullException(nameof(response)); } (string expectedKind, string expectedApiVersion) = KubeObjectV1.GetKubeKind <TResource>(); HttpResponseMessage responseMessage = null; try { responseMessage = await response; JObject responseJson = await responseMessage.ReadContentAsAsync <JObject, StatusV1>(successStatusCodes); string actualKind = responseJson.Value <string>("kind"); if (actualKind == null) { throw new KubeClientException($"Unable to {operationDescription}: received an invalid response from the Kubernetes API (expected a resource, but response was missing 'kind' property)."); } string actualApiVersion = responseJson.Value <string>("apiVersion"); if (actualKind == null) { throw new KubeClientException($"Unable to {operationDescription}: received an invalid response from the Kubernetes API (expected a resource, but response was missing 'apiVersion' property)."); } JsonSerializer serializer = responseMessage.GetJsonSerializer(); if ((actualKind, actualApiVersion) == (expectedKind, expectedApiVersion)) { return(serializer.Deserialize <TResource>(responseJson.CreateReader())); } else if ((actualKind, actualApiVersion) == ("Status", "v1")) { return(serializer.Deserialize <StatusV1>(responseJson.CreateReader())); } else { throw new KubeClientException($"Unable to {operationDescription}: received an unexpected response from the Kubernetes API (should be v1/Status or {expectedApiVersion}/{expectedKind}, but was {actualApiVersion}/{actualKind})."); } }
/// <summary> /// Get a single resource, returning <c>null</c> if it does not exist. /// </summary> /// <typeparam name="TResource"> /// The type of resource to retrieve. /// </typeparam> /// <param name="request"> /// An <see cref="HttpRequest"/> representing the resource to retrieve. /// </param> /// <param name="cancellationToken"> /// An optional <see cref="CancellationToken"/> that can be used to cancel the request. /// </param> /// <returns> /// A <typeparamref name="TResource"/> representing the current state for the resource, or <c>null</c> if no resource was found with the specified name and namespace. /// </returns> protected async Task <TResource> GetSingleResource <TResource>(HttpRequest request, CancellationToken cancellationToken = default) where TResource : KubeResourceV1 { if (request == null) { throw new ArgumentNullException(nameof(request)); } using (HttpResponseMessage responseMessage = await Http.GetAsync(request, cancellationToken).ConfigureAwait(false)) { if (responseMessage.IsSuccessStatusCode) { return(await responseMessage.ReadContentAsAsync <TResource>().ConfigureAwait(false)); } // Ensure that HttpStatusCode.NotFound actually refers to the target resource. StatusV1 status = await responseMessage.ReadContentAsStatusV1Async(HttpStatusCode.NotFound).ConfigureAwait(false); if (status.Reason == "NotFound") { return(null); } // If possible, tell the consumer which resource type we had a problem with (helpful when all you find is the error message in the log). (string itemKind, string itemApiVersion) = KubeObjectV1.GetKubeKind <TResource>(); string resourceTypeDescription = !String.IsNullOrWhiteSpace(itemKind) ? $"{itemKind} ({itemApiVersion}) resource" : typeof(TResource).Name; throw new KubeApiException($"Unable to retrieve {resourceTypeDescription} (HTTP status {responseMessage.StatusCode}).", innerException: new HttpRequestException <StatusV1>(responseMessage.StatusCode, response: await responseMessage.ReadContentAsStatusV1Async(responseMessage.StatusCode).ConfigureAwait(false) ) ); } }