From d8bb8a575e80bb5bc117c657730612e8173d4e27 Mon Sep 17 00:00:00 2001 From: Iskandar Abudiab Date: Sat, 4 Sep 2021 01:10:49 +0200 Subject: [PATCH] Add API for status and scale subresources --- ...ClusterScopedGenericKubernetesClient.swift | 54 +++ .../Client/GenericKubernetesClient.swift | 123 ++++-- ...amespacedGenericKubernetesClient+Pod.swift | 8 - .../NamespacedGenericKubernetesClient.swift | 68 +++- .../Client/RequestBuilder.swift | 358 ++++++++++++++---- .../RequestBuilderTests.swift | 108 ++++-- 6 files changed, 568 insertions(+), 151 deletions(-) diff --git a/Sources/SwiftkubeClient/Client/ClusterScopedGenericKubernetesClient.swift b/Sources/SwiftkubeClient/Client/ClusterScopedGenericKubernetesClient.swift index 5c9d1b7..e17495e 100644 --- a/Sources/SwiftkubeClient/Client/ClusterScopedGenericKubernetesClient.swift +++ b/Sources/SwiftkubeClient/Client/ClusterScopedGenericKubernetesClient.swift @@ -188,3 +188,57 @@ public extension ClusterScopedGenericKubernetesClient where Resource: Collection super.deleteAll(in: .allNamespaces) } } + +// MARK: - StatusHavingResource + +/// API functions for `StatusHavingResource`. +public extension ClusterScopedGenericKubernetesClient where Resource: StatusHavingResource { + + /// Loads an API resource's status by name. + /// + /// - Parameters: + /// - name: The name of the API resource to load. + /// + /// - Returns: An `EventLoopFuture` holding the API resource specified by the given name. + func getStatus(name: String) throws -> EventLoopFuture { + try super.getStatus(in: .allNamespaces, name: name) + } + + /// Replaces, i.e. updates, an API resource's status . + /// + /// - Parameters: + /// - name: The name of the resoruce to update. + /// - resource: A `KubernetesAPIResource` instance to update. + /// + /// - Returns: An `EventLoopFuture` holding the updated `KubernetesAPIResource`. + func updateStatus(name: String, _ resource: Resource) throws -> EventLoopFuture { + try super.updateStatus(in: .allNamespaces, name: name, resource) + } +} + +// MARK: - ScalableResource + +/// API functions for `ScalableResource`. +public extension ClusterScopedGenericKubernetesClient where Resource: ScalableResource { + + /// Loads an API resource's scale by name. + /// + /// - Parameters: + /// - name: The name of the API resource to load. + /// + /// - Returns: An `EventLoopFuture` holding the `autoscaling.v1.Scale` of the resource specified by the given name. + func getScale(name: String) throws -> EventLoopFuture { + try super.getScale(in: .allNamespaces, name: name) + } + + /// Replaces, i.e. updates, an API resource's scale. + /// + /// - Parameters: + /// - name: The name of the resoruce to update. + /// - resource: A `autoscaling.v1.Scale` instance to update. + /// + /// - Returns: An `EventLoopFuture` holding the updated `autoscaling.v1.Scale`. + func updateScale(name: String, scale: autoscaling.v1.Scale) throws -> EventLoopFuture { + try super.updateScale(in: .allNamespaces, name: name, scale: scale) + } +} diff --git a/Sources/SwiftkubeClient/Client/GenericKubernetesClient.swift b/Sources/SwiftkubeClient/Client/GenericKubernetesClient.swift index 7ae50df..8fa0eae 100644 --- a/Sources/SwiftkubeClient/Client/GenericKubernetesClient.swift +++ b/Sources/SwiftkubeClient/Client/GenericKubernetesClient.swift @@ -86,7 +86,7 @@ public class GenericKubernetesClient { public func get(in namespace: NamespaceSelector, name: String, options: [ReadOption]? = nil) -> EventLoopFuture { do { let eventLoop = httpClient.eventLoopGroup.next() - let request = try makeRequest().to(.GET).resource(withName: name).in(namespace).with(options: options).build() + let request = try makeRequest().in(namespace).toGet().resource(withName: name).with(options: options).build() return dispatch(request: request, eventLoop: eventLoop) } catch { @@ -106,7 +106,7 @@ public class GenericKubernetesClient { public func create(in namespace: NamespaceSelector, _ resource: Resource) -> EventLoopFuture { do { let eventLoop = httpClient.eventLoopGroup.next() - let request = try makeRequest().to(.POST).resource(resource).in(namespace).build() + let request = try makeRequest().in(namespace).toPost().body(resource).build() return dispatch(request: request, eventLoop: eventLoop) } catch { @@ -126,7 +126,7 @@ public class GenericKubernetesClient { public func update(in namespace: NamespaceSelector, _ resource: Resource) -> EventLoopFuture { do { let eventLoop = httpClient.eventLoopGroup.next() - let request = try makeRequest().to(.PUT).resource(withName: resource.name).resource(resource).in(namespace).build() + let request = try makeRequest().in(namespace).toPut().resource(withName: resource.name).body(.resource(payload: resource)).build() return dispatch(request: request, eventLoop: eventLoop) } catch { @@ -146,7 +146,7 @@ public class GenericKubernetesClient { public func delete(in namespace: NamespaceSelector, name: String, options: meta.v1.DeleteOptions?) -> EventLoopFuture> { do { let eventLoop = httpClient.eventLoopGroup.next() - let request = try makeRequest().to(.DELETE).resource(withName: name).in(namespace).build() + let request = try makeRequest().in(namespace).toDelete().resource(withName: name).build() return dispatch(request: request, eventLoop: eventLoop) } catch { @@ -164,7 +164,7 @@ public class GenericKubernetesClient { public func deleteAll(in namespace: NamespaceSelector) -> EventLoopFuture> { do { let eventLoop = httpClient.eventLoopGroup.next() - let request = try makeRequest().to(.DELETE).in(namespace).build() + let request = try makeRequest().in(namespace).toDelete().build() return dispatch(request: request, eventLoop: eventLoop) } catch { @@ -187,7 +187,87 @@ public extension GenericKubernetesClient where Resource: ListableResource { func list(in namespace: NamespaceSelector, options: [ListOption]? = nil) -> EventLoopFuture { do { let eventLoop = httpClient.eventLoopGroup.next() - let request = try makeRequest().to(.GET).in(namespace).with(options: options).build() + let request = try makeRequest().in(namespace).toGet().with(options: options).build() + + return dispatch(request: request, eventLoop: eventLoop) + } catch { + return httpClient.eventLoopGroup.next().makeFailedFuture(error) + } + } +} + +internal extension GenericKubernetesClient where Resource: ScalableResource { + + /// Reads a resource's scale in the given namespace. + /// + /// - Parameters: + /// - namespace: The namespace for this API request. + /// - name: The name of the resource to load. + /// + /// - Returns: An `EventLoopFuture` holding the `autoscaling.v1.Scale` for the desired resource . + func getScale(in namespace: NamespaceSelector, name: String) throws -> EventLoopFuture { + do { + let eventLoop = httpClient.eventLoopGroup.next() + let request = try makeRequest().in(namespace).toGet().resource(withName: name).subResource(.scale).build() + + return dispatch(request: request, eventLoop: eventLoop) + } catch { + return httpClient.eventLoopGroup.next().makeFailedFuture(error) + } + } + + /// Replaces the resource's scale in the given namespace. + /// + /// - Parameters: + /// - namespace: The namespace for this API request. + /// - name: The name of the resource to update. + /// - scale: An instance of `autoscaling.v1.Scale` to replace. + /// + /// - Returns: An `EventLoopFuture` holding the updated `autoscaling.v1.Scale` for the desired resource . + func updateScale(in namespace: NamespaceSelector, name: String, scale: autoscaling.v1.Scale) throws -> EventLoopFuture { + do { + let eventLoop = httpClient.eventLoopGroup.next() + let request = try makeRequest().in(namespace).toPut().resource(withName: name).body(.subResource(type: .scale, payload: scale)).build() + + return dispatch(request: request, eventLoop: eventLoop) + } catch { + return httpClient.eventLoopGroup.next().makeFailedFuture(error) + } + } +} + +internal extension GenericKubernetesClient where Resource: StatusHavingResource { + + /// Reads a resource's status in the given namespace. + /// + /// - Parameters: + /// - namespace: The namespace for this API request. + /// - name: The name of the resource to load. + /// + /// - Returns: An `EventLoopFuture` holding the `KubernetesAPIResource`. + func getStatus(in namespace: NamespaceSelector, name: String) throws -> EventLoopFuture { + do { + let eventLoop = httpClient.eventLoopGroup.next() + let request = try makeRequest().in(namespace).toGet().resource(withName: name).subResource(.status).build() + + return dispatch(request: request, eventLoop: eventLoop) + } catch { + return httpClient.eventLoopGroup.next().makeFailedFuture(error) + } + } + + /// Replaces the resource's status in the given namespace. + /// + /// - Parameters: + /// - namespace: The namespace for this API request. + /// - name: The name of the resource to update. + /// - resource: A `KubernetesAPIResource` instance to update. + /// + /// - Returns: An `EventLoopFuture` holding the updated `KubernetesAPIResource`. + func updateStatus(in namespace: NamespaceSelector, name: String, _ resource: Resource) throws -> EventLoopFuture { + do { + let eventLoop = httpClient.eventLoopGroup.next() + let request = try makeRequest().in(namespace).toPut().resource(withName: name).body(.subResource(type: .status, payload: resource)).build() return dispatch(request: request, eventLoop: eventLoop) } catch { @@ -198,7 +278,7 @@ public extension GenericKubernetesClient where Resource: ListableResource { internal extension GenericKubernetesClient { - func makeRequest() -> RequestBuilder { + func makeRequest() -> NamespaceStep { RequestBuilder(config: config, gvk: gvk) } @@ -263,31 +343,6 @@ internal extension GenericKubernetesClient { } } -internal extension GenericKubernetesClient { - - func status(in namespace: NamespaceSelector, name: String) throws -> EventLoopFuture { - do { - let eventLoop = httpClient.eventLoopGroup.next() - let request = try makeRequest().to(.GET).resource(withName: name).status().in(namespace).build() - - return dispatch(request: request, eventLoop: eventLoop) - } catch { - return httpClient.eventLoopGroup.next().makeFailedFuture(error) - } - } - - func updateStatus(in namespace: NamespaceSelector, _ resource: Resource) throws -> EventLoopFuture { - do { - let eventLoop = httpClient.eventLoopGroup.next() - let request = try makeRequest().to(.PUT).resource(resource).status().in(namespace).build() - - return dispatch(request: request, eventLoop: eventLoop) - } catch { - return httpClient.eventLoopGroup.next().makeFailedFuture(error) - } - } -} - internal extension GenericKubernetesClient { /// Watches the API resources in the given namespace. @@ -330,7 +385,7 @@ internal extension GenericKubernetesClient { retryStrategy: RetryStrategy = RetryStrategy(), using delegate: Delegate ) throws -> SwiftkubeClientTask { - let request = try makeRequest().toWatch().in(namespace).with(options: options).build() + let request = try makeRequest().in(namespace).toWatch().with(options: options).build() let watcher = ResourceWatcher(decoder: jsonDecoder, delegate: delegate) let clientDelegate = ClientStreamingDelegate(watcher: watcher, logger: logger) @@ -384,7 +439,7 @@ internal extension GenericKubernetesClient { retryStrategy: RetryStrategy = RetryStrategy.never, delegate: LogWatcherDelegate ) throws -> SwiftkubeClientTask { - let request = try makeRequest().toFollow(pod: name, container: container).in(namespace).build() + let request = try makeRequest().in(namespace).toFollow(pod: name, container: container).build() let watcher = LogWatcher(delegate: delegate) let clientDelegate = ClientStreamingDelegate(watcher: watcher, logger: logger) diff --git a/Sources/SwiftkubeClient/Client/NamespacedGenericKubernetesClient+Pod.swift b/Sources/SwiftkubeClient/Client/NamespacedGenericKubernetesClient+Pod.swift index 7e67f1c..d063696 100644 --- a/Sources/SwiftkubeClient/Client/NamespacedGenericKubernetesClient+Pod.swift +++ b/Sources/SwiftkubeClient/Client/NamespacedGenericKubernetesClient+Pod.swift @@ -36,12 +36,4 @@ public extension NamespacedGenericKubernetesClient where Resource == core.v1.Pod delegate: delegate ) } - - func status(in namespace: NamespaceSelector? = nil, name: String) throws -> EventLoopFuture { - try super.status(in: namespace ?? .namespace(config.namespace), name: name) - } - - func updateStatus(in namespace: NamespaceSelector? = nil, _ pod: core.v1.Pod) throws -> EventLoopFuture { - try super.updateStatus(in: namespace ?? .namespace(config.namespace), pod) - } } diff --git a/Sources/SwiftkubeClient/Client/NamespacedGenericKubernetesClient.swift b/Sources/SwiftkubeClient/Client/NamespacedGenericKubernetesClient.swift index 0671e33..7ac6f3e 100644 --- a/Sources/SwiftkubeClient/Client/NamespacedGenericKubernetesClient.swift +++ b/Sources/SwiftkubeClient/Client/NamespacedGenericKubernetesClient.swift @@ -191,7 +191,7 @@ public extension NamespacedGenericKubernetesClient where Resource: ReplaceableRe /// - namespace: The namespace for this API request. /// - resource: A `KubernetesAPIResource` instance to update. /// - /// - Returns: An `EventLoopFuture` holding the created `KubernetesAPIResource`. + /// - Returns: An `EventLoopFuture` holding the updated `KubernetesAPIResource`. func update(inNamespace namespace: NamespaceSelector? = nil, _ resource: Resource) -> EventLoopFuture { super.update(in: namespace ?? .namespace(config.namespace), resource) } @@ -233,3 +233,69 @@ public extension NamespacedGenericKubernetesClient where Resource: CollectionDel super.deleteAll(in: namespace ?? .namespace(config.namespace)) } } + +// MARK: - StatusHavingResource + +/// API functions for `StatusHavingResource`. +public extension NamespacedGenericKubernetesClient where Resource: StatusHavingResource { + + /// Loads an API resource's status by name in the given namespace. + /// + /// If the namespace is not specified then the default namespace defined in the `KubernetesClientConfig` will be used instead. + /// + /// - Parameters: + /// - namespace: The namespace for this API request. + /// - name: The name of the API resource to load. + /// + /// - Returns: An `EventLoopFuture` holding the API resource specified by the given name in the given namespace. + func getStatus(in namespace: NamespaceSelector? = nil, name: String) throws -> EventLoopFuture { + try super.getStatus(in: namespace ?? .namespace(config.namespace), name: name) + } + + /// Replaces, i.e. updates, an API resource's status in the given namespace. + /// + /// If the namespace is not specified then the default namespace defined in the `KubernetesClientConfig` will be used instead. + /// + /// - Parameters: + /// - namespace: The namespace for this API request. + /// - name: The name of the resoruce to update. + /// - resource: A `KubernetesAPIResource` instance to update. + /// + /// - Returns: An `EventLoopFuture` holding the updated `KubernetesAPIResource`. + func updateStatus(in namespace: NamespaceSelector? = nil, name: String, _ resource: Resource) throws -> EventLoopFuture { + try super.updateStatus(in: namespace ?? .namespace(config.namespace), name: name, resource) + } +} + +// MARK: - ScalableResource + +/// API functions for `ScalableResource`. +public extension NamespacedGenericKubernetesClient where Resource: ScalableResource { + + /// Loads an API resource's scale by name in the given namespace. + /// + /// If the namespace is not specified then the default namespace defined in the `KubernetesClientConfig` will be used instead. + /// + /// - Parameters: + /// - namespace: The namespace for this API request. + /// - name: The name of the API resource to load. + /// + /// - Returns: An `EventLoopFuture` holding the `autoscaling.v1.Scale` of the resource specified by the given name in the given namespace. + func getScale(in namespace: NamespaceSelector? = nil, name: String) throws -> EventLoopFuture { + try super.getScale(in: namespace ?? .namespace(config.namespace), name: name) + } + + /// Replaces, i.e. updates, an API resource's scale in the given namespace. + /// + /// If the namespace is not specified then the default namespace defined in the `KubernetesClientConfig` will be used instead. + /// + /// - Parameters: + /// - namespace: The namespace for this API request. + /// - name: The name of the resoruce to update. + /// - resource: A `autoscaling.v1.Scale` instance to update. + /// + /// - Returns: An `EventLoopFuture` holding the updated `autoscaling.v1.Scale`. + func updateScale(in namespace: NamespaceSelector? = nil, name: String, scale: autoscaling.v1.Scale) throws -> EventLoopFuture { + try super.updateScale(in: namespace ?? .namespace(config.namespace), name: name, scale: scale) + } +} diff --git a/Sources/SwiftkubeClient/Client/RequestBuilder.swift b/Sources/SwiftkubeClient/Client/RequestBuilder.swift index c484711..75161b1 100644 --- a/Sources/SwiftkubeClient/Client/RequestBuilder.swift +++ b/Sources/SwiftkubeClient/Client/RequestBuilder.swift @@ -20,119 +20,325 @@ import NIO import NIOHTTP1 import SwiftkubeModel -internal extension HTTPMethod { +// MARK: - ResourceType - var hasRequestBody: Bool { +internal enum ResourceType { + case root, log, scale, status + + var path: String { switch self { - case .POST, .PUT, .PATCH: - return true - default: - return false + case .root: + return "" + case .log: + return "/log" + case .scale: + return "/scale" + case .status: + return "/status" } } } +// MARK: - RequestBody + +internal enum RequestBody { + case resource(payload: KubernetesAPIResource) + case subResource(type: ResourceType, payload: KubernetesResource) + + var type: ResourceType { + switch self { + case .resource: + return .root + case let .subResource(type: subType, payload: _): + return subType + } + } + + var payload: KubernetesResource { + switch self { + case let .resource(payload: payload): + return payload + case let .subResource(type: _, payload: payload): + return payload + } + } +} + +// MARK: - NamespaceStep + +internal protocol NamespaceStep { + func `in`(_ namespace: NamespaceSelector) -> MethodStep +} + +// MARK: - MethodStep + +internal protocol MethodStep { + func toGet() -> GetStep + func toWatch() -> GetStep + func toFollow(pod: String, container: String?) -> GetStep + func toPost() -> PostStep + func toPut() -> PutStep + func toDelete() -> DeleteStep +} + +// MARK: - GetStep + +internal protocol GetStep { + func resource(withName name: String?) -> GetStep + func subResource(_ subType: ResourceType) -> GetStep + func with(options: [ListOption]?) -> GetStep + func with(options: [ReadOption]?) -> GetStep + func build() throws -> HTTPClient.Request +} + +// MARK: - PostStep + +internal protocol PostStep { + func body(_ resource: Resource) -> PostStep + func build() throws -> HTTPClient.Request +} + +// MARK: - PutStep + +internal protocol PutStep { + func resource(withName name: String?) -> PutStep + func body(_ body: RequestBody) -> PutStep + func build() throws -> HTTPClient.Request +} + +// MARK: - DeleteStep + +internal protocol DeleteStep { + func resource(withName name: String?) -> DeleteStep + func with(options: meta.v1.DeleteOptions?) -> DeleteStep + func build() throws -> HTTPClient.Request +} + // MARK: - RequestBuilder /// /// An internal class for building API request objects. /// /// It assumes a correct usage and does only minimal sanity checks. -internal class RequestBuilder { +internal class RequestBuilder { let config: KubernetesClientConfig let gvk: GroupVersionKind var components: URLComponents? - var method: HTTPMethod! var namespace: NamespaceSelector! - var resource: Resource? + var method: HTTPMethod! { + didSet { + switch method { + case .POST, .PUT, .PATCH: + hasPayload = true + default: + hasPayload = false + } + } + } + + var hasPayload: Bool = false + var resourceName: String? + var requestBody: RequestBody? { + didSet { + subResourceType = requestBody?.type + } + } + + var subResourceType: ResourceType? + + var containerName: String? var listOptions: [ListOption]? var readOptions: [ReadOption]? var deleteOptions: meta.v1.DeleteOptions? - var statusRequest: Bool = false - var watchRequest: Bool = false - var followRequest: Bool = false - var container: String? + var watchFlag: Bool = false init(config: KubernetesClientConfig, gvk: GroupVersionKind) { self.config = config self.gvk = gvk self.components = URLComponents(url: config.masterURL, resolvingAgainstBaseURL: false) } +} + +// MARK: NamespaceStep + +extension RequestBuilder: NamespaceStep { + + /// Set the namespace for the pending request and move to the Method Step + /// - Parameter namespace: The namespace for this request + /// - Returns: The builder instance as MethodStep + func `in`(_ namespace: NamespaceSelector) -> MethodStep { + self.namespace = namespace + return self as MethodStep + } +} + +// MARK: MethodStep + +extension RequestBuilder: MethodStep { + + /// Set request method to GET for the pending request + /// - Returns:The builder instance as GetStep + func toGet() -> GetStep { + method = .GET + return self as GetStep + } + + /// Set request method to POST for the pending request + /// - Returns:The builder instance as PostStep + func toPost() -> PostStep { + method = .POST + return self as PostStep + } - func to(_ method: HTTPMethod) -> RequestBuilder { - self.method = method - return self + /// Set request method to PUT for the pending request + /// - Returns:The builder instance as PutStep + func toPut() -> PutStep { + method = .PUT + return self as PutStep } - func status() -> RequestBuilder { - statusRequest = true - return self + /// Set request method to DELETE for the pending request + /// - Returns:The builder instance as DeleteStep + func toDelete() -> DeleteStep { + method = .DELETE + return self as DeleteStep } - func toWatch() -> RequestBuilder { + /// Set request method to GET and toggle the `watch` flag + /// - Returns:The builder instance as GetStep + func toWatch() -> GetStep { method = .GET - watchRequest = true - return self + watchFlag = true + return self as GetStep } - func toFollow(pod: String, container: String?) -> RequestBuilder { + /// Set request method to GET and notice the pod and container to follow for the pending request + /// - Returns:The builder instance as GetStep + func toFollow(pod: String, container: String?) -> GetStep { method = .GET resourceName = pod - self.container = container - followRequest = true - return self + containerName = container + subResourceType = .log + return self as GetStep } +} - func resource(_ resource: Resource) -> RequestBuilder { - self.resource = resource - return self - } +// MARK: GetStep + +extension RequestBuilder: GetStep { - func resource(withName name: String?) -> RequestBuilder { + /// Set the name of the resource for the pending request + /// - Parameter name: The name of the resource + /// - Returns: The builder instance as GetStep + func resource(withName name: String?) -> GetStep { resourceName = name - return self + return self as GetStep } - func `in`(_ namespace: NamespaceSelector) -> RequestBuilder { - self.namespace = namespace - return self + /// Set the sub-reousrce type for the pending request + /// - Parameter subType: The `ResourceType` + /// - Returns: The builder instance as GetStep + func subResource(_ subType: ResourceType) -> GetStep { + subResourceType = subType + return self as GetStep } - func with(options: [ListOption]?) -> RequestBuilder { + /// Set the `ListOptions` for the pending request + /// - Parameter options: The `ListOptions` + /// - Returns: The builder instance as GetStep + func with(options: [ListOption]?) -> GetStep { listOptions = options - return self + return self as GetStep } - func with(options: [ReadOption]?) -> RequestBuilder { + /// Set the `ReadOption` for the pending request + /// - Parameter options: The `ReadOption` + /// - Returns: The builder instance as GetStep + func with(options: [ReadOption]?) -> GetStep { readOptions = options - return self + return self as GetStep + } +} + +// MARK: PostStep + +extension RequestBuilder: PostStep { + + /// Set the body payload for the pending request + /// - Parameter resource: The `KubernetesAPIResource` payload + /// - Returns: The builder instance as PostStep + func body(_ resource: Resource) -> PostStep { + requestBody = .resource(payload: resource) + return self as PostStep + } +} + +// MARK: PutStep + +extension RequestBuilder: PutStep { + + /// Set the name of the resource for the pending request + /// - Parameter name: The name of the resource + /// - Returns: The builder instance as PutStep + func resource(withName name: String?) -> PutStep { + resourceName = name + return self as PutStep } - func with(options: meta.v1.DeleteOptions?) -> RequestBuilder { + /// Set the body payload for the pending request + /// - Parameter resource: The `KubernetesAPIResource` payload + /// - Returns: The builder instance as PostStep + func body(_ body: RequestBody) -> PutStep { + requestBody = body + return self as PutStep + } +} + +// MARK: DeleteStep + +extension RequestBuilder: DeleteStep { + + /// Set the name of the resource for the pending request + /// - Parameter name: The name of the resource + /// - Returns: The builder instance as DeleteStep + func resource(withName name: String?) -> DeleteStep { + resourceName = name + return self as DeleteStep + } + + /// Set the `DeleteOptions` for the pending request + /// - Parameter options: The `DeleteOptions` + /// - Returns: The builder instance as DeleteStep + func with(options: meta.v1.DeleteOptions?) -> DeleteStep { deleteOptions = options - return self + return self as DeleteStep } +} + +internal extension RequestBuilder { func build() throws -> HTTPClient.Request { components?.path = urlPath(forNamespace: namespace, name: resourceName) - if statusRequest { - components?.path += "/status" + if let subResourceType = subResourceType { + components?.path += subResourceType.path } - if followRequest { - components?.path += "/log" + if requestBody?.type == .root { + guard + let body = requestBody, + case let RequestBody.resource(payload: payload) = body, + payload.name != nil + else { + throw SwiftkubeClientError.badRequest("Resource `metadata.name` must be set.") + } } - guard !(method.hasRequestBody && resource?.name == nil) else { - throw SwiftkubeClientError.badRequest("Resource `metadata.name` must be set.") - } - - guard !(method == .DELETE && resource != nil) else { - throw SwiftkubeClientError.badRequest("Resource can't be set for DELETE call.") + guard !(method == .DELETE && requestBody != nil) else { + throw SwiftkubeClientError.badRequest("RequestBody can't be set for DELETE call.") } if let readOptions = readOptions { @@ -143,15 +349,15 @@ internal class RequestBuilder { listOptions.collectQueryItems().forEach(add(queryItem:)) } - if watchRequest { + if watchFlag { add(queryItem: URLQueryItem(name: "watch", value: "true")) } - if followRequest { + if subResourceType == .log { add(queryItem: URLQueryItem(name: "follow", value: "true")) } - if let container = container { + if let container = containerName { add(queryItem: URLQueryItem(name: "container", value: container)) } @@ -160,25 +366,12 @@ internal class RequestBuilder { } let headers = buildHeaders(withAuthentication: config.authentication) - var body: HTTPClient.Body? - - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 - - if let resource = resource { - let data = try encoder.encode(resource) - body = .data(data) - } - - if let options = deleteOptions { - let data = try encoder.encode(options) - body = .data(data) - } + let body = try buildBody() return try HTTPClient.Request(url: url, method: method, headers: headers, body: body) } - func urlPath(forNamespace namespace: NamespaceSelector, name: String?) -> String { + private func urlPath(forNamespace namespace: NamespaceSelector, name: String?) -> String { var url: String if case NamespaceSelector.allNamespaces = namespace { @@ -194,14 +387,14 @@ internal class RequestBuilder { return url } - func add(queryItem: URLQueryItem) { + private func add(queryItem: URLQueryItem) { if components?.queryItems == nil { components?.queryItems = [] } components?.queryItems?.append(queryItem) } - func buildHeaders(withAuthentication authentication: KubernetesClientAuthentication?) -> HTTPHeaders { + private func buildHeaders(withAuthentication authentication: KubernetesClientAuthentication?) -> HTTPHeaders { var headers: [(String, String)] = [] if let authorizationHeader = authentication?.authorizationHeader() { headers.append(("Authorization", authorizationHeader)) @@ -209,4 +402,27 @@ internal class RequestBuilder { return HTTPHeaders(headers) } + + private func buildBody() throws -> HTTPClient.Body? { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + + if let requestBody = requestBody { + let data = try requestBody.payload.encode(encoder: encoder) + return .data(data) + } + + if let options = deleteOptions { + let data = try encoder.encode(options) + return .data(data) + } + + return nil + } +} + +private extension KubernetesResource { + func encode(encoder: JSONEncoder) throws -> Data { + try encoder.encode(self) + } } diff --git a/Tests/SwiftkubeClientTests/RequestBuilderTests.swift b/Tests/SwiftkubeClientTests/RequestBuilderTests.swift index 954267d..f3e717b 100644 --- a/Tests/SwiftkubeClientTests/RequestBuilderTests.swift +++ b/Tests/SwiftkubeClientTests/RequestBuilderTests.swift @@ -39,50 +39,50 @@ final class RequestBuilderTests: XCTestCase { } func testGetInNamespace() { - let builder = RequestBuilder(config: config, gvk: gvk) - var request = try? builder.to(.GET).in(.default).build() + let builder = RequestBuilder(config: config, gvk: gvk) + var request = try? builder.in(.default).toGet().build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/default/pods")!) XCTAssertEqual(request?.method, HTTPMethod.GET) - request = try? builder.to(.GET).in(.system).build() + request = try? builder.in(.system).toGet().build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/kube-system/pods")!) XCTAssertEqual(request?.method, HTTPMethod.GET) } func testGetInAllNamespaces() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.GET).in(.allNamespaces).build() + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.allNamespaces).toGet().build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/pods")!) XCTAssertEqual(request?.method, HTTPMethod.GET) } func testGetInNamespaceWithName() { - let builder = RequestBuilder(config: config, gvk: gvk) - var request = try? builder.to(.GET).in(.default).resource(withName: "test").build() + let builder = RequestBuilder(config: config, gvk: gvk) + var request = try? builder.in(.default).toGet().resource(withName: "test").build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/default/pods/test")!) XCTAssertEqual(request?.method, HTTPMethod.GET) - request = try? builder.to(.GET).in(.system).build() + request = try? builder.in(.system).toGet().build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/kube-system/pods/test")!) XCTAssertEqual(request?.method, HTTPMethod.GET) } func testFollowInNamespace() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.toFollow(pod: "pod", container: "container").in(.system).build() + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.system).toFollow(pod: "pod", container: "container").build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/kube-system/pods/pod/log?follow=true&container=container")!) XCTAssertEqual(request?.method, HTTPMethod.GET) } func testGetWithListOptions_Eq() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.GET).in(.default).with(options: [ + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().with(options: [ .labelSelector(.eq(["app": "nginx"])), ]) .build() @@ -91,8 +91,8 @@ final class RequestBuilderTests: XCTestCase { } func testGetWithListOptions_NotEq() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.GET).in(.default).with(options: [ + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().with(options: [ .labelSelector(.neq(["app": "nginx"])), ]) .build() @@ -101,8 +101,8 @@ final class RequestBuilderTests: XCTestCase { } func testGetWithListOptions_In() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.GET).in(.default).with(options: [ + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().with(options: [ .labelSelector(.in(["env": ["dev", "staging"]])), ]) .build() @@ -110,8 +110,8 @@ final class RequestBuilderTests: XCTestCase { } func testGetWithListOptions_NotIn() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.GET).in(.default).with(options: [ + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().with(options: [ .labelSelector(.notIn(["env": ["dev", "staging"]])), ]) .build() @@ -119,8 +119,8 @@ final class RequestBuilderTests: XCTestCase { } func testGetWithListOptions_Exists() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.GET).in(.default).with(options: [ + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().with(options: [ .labelSelector(.exists(["app", "env"])), ]) .build() @@ -129,8 +129,8 @@ final class RequestBuilderTests: XCTestCase { } func testGetWithListOptions_FieldEq() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.GET).in(.default).with(options: [ + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().with(options: [ .fieldSelector(.eq(["app": "nginx"])), ]) .build() @@ -139,8 +139,8 @@ final class RequestBuilderTests: XCTestCase { } func testGetWithListOptions_FieldNotEq() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.GET).in(.default).with(options: [ + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().with(options: [ .fieldSelector(.neq(["app": "nginx"])), ]) .build() @@ -149,8 +149,8 @@ final class RequestBuilderTests: XCTestCase { } func testGetWithListOptions_Limit() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.GET).in(.default).with(options: [ + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().with(options: [ .limit(2), ]) .build() @@ -159,8 +159,8 @@ final class RequestBuilderTests: XCTestCase { } func testGetWithListOptions_Version() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.GET).in(.default).with(options: [ + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().with(options: [ .resourceVersion("20"), ]) .build() @@ -168,42 +168,76 @@ final class RequestBuilderTests: XCTestCase { XCTAssertEqual(request?.url.query, "resourceVersion=20") } + func testGetStatus() { + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().resource(withName: "test").subResource(.status).build() + + XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/default/pods/test/status")!) + XCTAssertEqual(request?.method, HTTPMethod.GET) + } + + func testGetScale() { + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.default).toGet().resource(withName: "test").subResource(.scale).build() + + XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/default/pods/test/scale")!) + XCTAssertEqual(request?.method, HTTPMethod.GET) + } + func testDeleteInNamespace() { - let builder = RequestBuilder(config: config, gvk: gvk) - var request = try? builder.to(.DELETE).resource(withName: "test").in(.default).build() + let builder = RequestBuilder(config: config, gvk: gvk) + var request = try? builder.in(.default).toDelete().resource(withName: "test").build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/default/pods/test")!) XCTAssertEqual(request?.method, HTTPMethod.DELETE) - request = try? builder.to(.DELETE).in(.system).build() + request = try? builder.in(.system).toDelete().build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/kube-system/pods/test")!) XCTAssertEqual(request?.method, HTTPMethod.DELETE) } func testDeleteInAllNamespaces() { - let builder = RequestBuilder(config: config, gvk: gvk) - let request = try? builder.to(.DELETE).in(.allNamespaces).build() + let builder = RequestBuilder(config: config, gvk: gvk) + let request = try? builder.in(.allNamespaces).toDelete().build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/pods")!) XCTAssertEqual(request?.method, HTTPMethod.DELETE) } func testCreateInNamespace() { - let builder = RequestBuilder(config: config, gvk: gvk) + let builder = RequestBuilder(config: config, gvk: gvk) let pod = sk.pod(name: "test") - let request = try? builder.to(.POST).resource(pod).in(.default).build() + let request = try? builder.in(.default).toPost().body(pod).build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/default/pods")!) XCTAssertEqual(request?.method, HTTPMethod.POST) } func testReplaceInNamespace() { - let builder = RequestBuilder(config: config, gvk: gvk) + let builder = RequestBuilder(config: config, gvk: gvk) let pod = sk.pod(name: "test") - let request = try? builder.to(.PUT).resource(pod).resource(withName: "test").in(.default).build() + let request = try? builder.in(.default).toPut().resource(withName: "test").body(.resource(payload: pod)).build() XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/default/pods/test")!) XCTAssertEqual(request?.method, HTTPMethod.PUT) } + + func testReplaceStatusInNamespace() { + let builder = RequestBuilder(config: config, gvk: gvk) + let pod = sk.pod(name: "test") + let request = try? builder.in(.default).toPut().resource(withName: "test").body(.subResource(type: .status, payload: pod)).build() + + XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/default/pods/test/status")!) + XCTAssertEqual(request?.method, HTTPMethod.PUT) + } + + func testReplaceScaleInNamespace() { + let builder = RequestBuilder(config: config, gvk: gvk) + let pod = sk.pod(name: "test") + let request = try? builder.in(.default).toPut().resource(withName: "test").body(.subResource(type: .scale, payload: pod)).build() + + XCTAssertEqual(request?.url, URL(string: "https://kubernetesmaster/api/v1/namespaces/default/pods/test/scale")!) + XCTAssertEqual(request?.method, HTTPMethod.PUT) + } }