diff --git a/CHANGELOG.md b/CHANGELOG.md index 02a2a314ea2..53bf3724f21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * Remove `setIntVal`, `setStrVal`, `setKind` setters from `IntOrString` class to avoid invalid combinations * Fix #3889 : remove piped stream for file download * Fix #1285: removed references to manually calling registerCustomKind +* Fix #3334: adding basic support for server side apply. Use patch(PatchContext.of(PatchType.SERVER_SIDE_APPLY), service), or new PatchContext.Builder().withPatchType(PatchType.SERVER_SIDE_APPLY).withForce(true).build() to override conflicts. #### Dependency Upgrade * Fix #3788: Point CamelK Extension model to latest released version v1.8.0 diff --git a/doc/CHEATSHEET.md b/doc/CHEATSHEET.md index 6d57d2118c1..c31903b7e5f 100644 --- a/doc/CHEATSHEET.md +++ b/doc/CHEATSHEET.md @@ -47,6 +47,7 @@ This document contains common usages of different resources using Fabric8 Kubern * [Log Options](#log-options) * [Serializing to yaml](#serializing-to-yaml) * [Running a Pod](#running-a-pod) + * [Server Side Apply](#server-side-apply) * [OpenShift Client DSL Usage](#openshift-client-dsl-usage) * [Initializing OpenShift Client](#initializing-openshift-client) @@ -74,16 +75,16 @@ This document contains common usages of different resources using Fabric8 Kubern ### Initializing Kubernetes Client Typically, we create Kubernetes Client like this: ``` -try (final KubernetesClient client = new DefaultKubernetesClient()) { +try (final KubernetesClient client = new KubernetesClientBuilder().build()) { // Do stuff with client } ``` -This would pick up default settings, reading your `kubeconfig` file from `~/.kube/config` directory or whatever is defined inside `KUBECONFIG` environment variable. But if you want to customize creation of client, you can also pass a `Config` object inside `DefaultKubernetesClient` like this: +This would pick up default settings, reading your `kubeconfig` file from `~/.kube/config` directory or whatever is defined inside `KUBECONFIG` environment variable. But if you want to customize creation of client, you can also pass a `Config` object inside the builder like this: ``` Config kubeConfig = new ConfigBuilder() .withMasterUrl("https://192.168.42.20:8443/") .build() -try (final KubernetesClient client = new DefaultKubernetesClient(kubeConfig)) { +try (final KubernetesClient client = new KubernetesClientBuilder().withConfig(kubeConfig).build()) { // Do stuff with client } ``` @@ -196,7 +197,7 @@ deleteLatch.await(10, TimeUnit.MINUTES) When trying to access Kubernetes API from within a `Pod` authentication is done a bit differently as compared to when being done on your system. If you checkout [documentation](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/#accessing-the-api-from-a-pod). Client authenticates by reading `ServiceAccount` from `/var/run/secrets/kubernetes.io/serviceaccount/` and reads environment variables like `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` for apiServer URL. You don't have to worry about all this when using Fabric8 Kubernetes Client. You can simply use it like this and client will take care of everything: ``` // reads serviceaccount from mounted volume and gets apiServer url from environment variables itself. -KubernetesClient client = new DefaultKubernetesClient(); +KubernetesClient client = new KubernetesClientBuilder().build(); ``` You can also checkout a demo example here: [kubernetes-client-inside-pod](https://github.com/rohanKanojia/kubernetes-client-inside-pod) @@ -1449,7 +1450,7 @@ Boolean deleted = client.policy().podDisruptionBudget().inNamespace("default").w ### SelfSubjectAccessReview - Create `SelfSubjectAccessReview`(equivalent of `kubectl auth can-i create deployments --namespace dev`): ``` -try (KubernetesClient client = new DefaultKubernetesClient()) { +try (KubernetesClient client = new KubernetesClientBuilder().build()) { SelfSubjectAccessReview ssar = new SelfSubjectAccessReviewBuilder() .withNewSpec() .withNewResourceAttributes() @@ -1470,7 +1471,7 @@ try (KubernetesClient client = new DefaultKubernetesClient()) { ### SubjectAccessReview - Create `SubjectAccessReview`: ``` -try (KubernetesClient client = new DefaultKubernetesClient()) { +try (KubernetesClient client = new KubernetesClientBuilder().build()) { SubjectAccessReview sar = new SubjectAccessReviewBuilder() .withNewSpec() .withNewResourceAttributes() @@ -1491,7 +1492,7 @@ try (KubernetesClient client = new DefaultKubernetesClient()) { ### LocalSubjectAccessReview - Create `LocalSubjectAccessReview`: ``` -try (KubernetesClient client = new DefaultKubernetesClient()) { +try (KubernetesClient client = new KubernetesClientBuilder().build()) { LocalSubjectAccessReview lsar = new LocalSubjectAccessReviewBuilder() .withNewMetadata().withNamespace("default").endMetadata() .withNewSpec() @@ -1512,7 +1513,7 @@ try (KubernetesClient client = new DefaultKubernetesClient()) { ### SelfSubjectRulesReview - Create `SelfSubjectRulesReview`: ``` -try (KubernetesClient client = new DefaultKubernetesClient()) { +try (KubernetesClient client = new KubernetesClientBuilder().build()) { SelfSubjectRulesReview selfSubjectRulesReview = new SelfSubjectRulesReviewBuilder() .withNewMetadata().withName("foo").endMetadata() .withNewSpec() @@ -1880,7 +1881,13 @@ cronTabClient.inNamespace("default").watch(new Watcher() { ``` ### Resource Typeless API -If you don't need or want to use a strongly typed client, the Kubernetes Client also provides a typeless/raw API to handle your resources in form of GenericKubernetesResource, which implements HasMetadata and provides the rest of its fields via a map. In order to use it, you need to provide a `ResourceDefinitionContext`, which carries necessary information about the resource. Here is an example on how to create one: +If you don't need or want to use a strongly typed client, the Kubernetes Client also provides a typeless/raw API to handle your resources in form of GenericKubernetesResource. GenericKubernetesResource implements HasMetadata and provides the rest of its fields via a map. In most circumstances the client can infer the necessary details about your type from the api server, this includes: + +* client.genericKuberetesResources(apiVersion, kind) - to perform operations generically +* client.resource(resource) - if you already constructed an instance of your GenericKubernetesResource +* any of the load and related methods - if you have the yaml/json of a resource, but there is no class defined for deserializing it. + +In some circumstances, such as an error with the logic automatically inferring the type details or when trying to use built-in mock support for the implicit generic scenario, you will need to use you will need to provide a `ResourceDefinitionContext`, which carries necessary information about the resource. Here is an example on how to create one: - Create `ResourceDefinitionContext`: ```java ResourceDefinitionContext resourceDefinitionContext = new ResourceDefinitionContext.Builder() @@ -1891,7 +1898,9 @@ ResourceDefinitionContext resourceDefinitionContext = new ResourceDefinitionCont .withNamespaced(true) .build(); ``` -Once you have built it, you can pass it to typeless DSL as argument `client.genericKubernetesResources(resourceDefinitionContext)`. With this in place, you can do your standard operations. +Once you have built it, you instead use `client.genericKubernetesResources(resourceDefinitionContext)` as your api entry point. + +Explicit usage examples: - Load a resource from yaml: ```java @@ -1962,7 +1971,7 @@ closeLatch.await(10, TimeUnit.MINUTES); Kubernetes Client provides using `CertificateSigningRequest` via the `client.certificates().v1().certificateSigningRequests()` DSL interface. Here is an example of creating `CertificateSigningRequest` using Fabric8 Kubernetes Client: - Create `CertificateSigningRequest`: ``` -try (KubernetesClient client = new DefaultKubernetesClient()) { +try (KubernetesClient client = new KubernetesClientBuilder().build()) { CertificateSigningRequest csr = new CertificateSigningRequestBuilder() .withNewMetadata().withName("test-k8s-csr").endMetadata() .withNewSpec() @@ -2304,7 +2313,7 @@ String myPodAsYamlWithoutRuntimeState = SerializationUtils.dumpWithoutRuntimeSta Kubernetes Client also provides mechanism similar to `kubectl run` in which you can spin a `Pod` just by specifying it's image and name: - Running a `Pod` by just providing image and name: ``` -try (KubernetesClient client = new DefaultKubernetesClient()) { +try (KubernetesClient client = new KubernetesClientBuilder().build()) { client.run().inNamespace("default") .withName("hazelcast") .withImage("hazelcast/hazelcast:3.12.9") @@ -2313,7 +2322,7 @@ try (KubernetesClient client = new DefaultKubernetesClient()) { ``` - You can also provide slighly complex configuration with `withGeneratorConfig` method in which you can specify labels, environment variables, ports etc: ``` -try (KubernetesClient client = new DefaultKubernetesClient()) { +try (KubernetesClient client = new KubernetesClientBuilder().build()) { client.run().inNamespace("default") .withRunConfig(new RunConfigBuilder() .withName("nginx") @@ -2325,32 +2334,45 @@ try (KubernetesClient client = new DefaultKubernetesClient()) { } ``` +#### Server Side Apply + +Basic usage of server side apply is available via Patchable. At it's simplest you just need to call: + +``` +client.services().withName("name").patch(PatchContext.of(PatchType.SERVER_SIDE_APPLY), service); +``` + +For any create or update. This can be a good alternative to using createOrReplace as it is always a single api call and does not issue a replace/PUT which can be problematic. + +If the resources may be created or modified by something other than a fabric8 patch, you will need to force your modifications: + +``` +client.services().withName("name").patch(new PatchContext.Builder().withPatchType(PatchType.SERVER_SIDE_APPLY).withForce(true).build(), service); +``` + +Please consult the Kubernetes server side apply documentation if you want to do more detailed field management or want to understand the full semantics of how the patches are merged. + ### OpenShift Client DSL Usage Fabric8 Kubernetes Client also has an extension for OpenShift. It is pretty much the same as Kubernetes Client but has support for some additional OpenShift resources. #### Initializing OpenShift Client: -Initializing OpenShift client is the same as Kubernetes Client. Yo +Initializing OpenShift client is the same as Kubernetes Client. You use ``` -try (final OpenShiftClient client = new DefaultOpenShiftClient()) { +try (final OpenShiftClient client = new KubernetesClientBuilder().build().adapt(OpenShiftClient.class)) { // Do stuff with client } ``` -This would pick up default settings, reading your `kubeconfig` file from `~/.kube/config` directory or whatever is defined inside `KUBECONFIG` environment variable. But if you want to customize creation of client, you can also pass a `Config` object inside `DefaultKubernetesClient` like this: +This would pick up default settings, reading your `kubeconfig` file from `~/.kube/config` directory or whatever is defined inside `KUBECONFIG` environment variable. But if you want to customize creation of client, you can also pass a `Config` object inside the builder like this: ``` Config kubeConfig = new ConfigBuilder() .withMasterUrl("https://api.ci-ln-3sbdl1b-d5d6b.origin-ci-int-aws.dev.examplecloud.com:6443") .withOauthToken("xxxxxxxx-41oafKI6iU637-xxxxxxxxxxxxx") .build())) { -try (final OpenShiftClient client = new DefaultOpenShiftClient(config)) { +try (final OpenShiftClient client = new KubernetesClientBuilder().withConfig(kubeConfig).build().adapt(OpenShiftClient.class)) { // Do stuff with client } ``` -You can also create `OpenShiftClient` from an existing instance of `KubernetesClient`. There is a method called `adapt(..)` for this. Here is an example: -``` -KubernetesClient client = new DefaultKubernetesClient(); -OpenShiftClient openShiftClient = client.adapt(OpenShiftClient.class); -``` #### DeploymentConfig `DeploymentConfig` is available in OpenShift client via `client.deploymentConfigs()`. Here are some examples of its common usage: @@ -2757,7 +2779,7 @@ client.operatorHub().monitoring().inNamespace("default").withName("foo").delete( #### ClusterResourceQuota - Create `ClusterResourceQuota`: ``` -try (OpenShiftClient client = new DefaultOpenShiftClient()) { +try (OpenShiftClient client = new KubernetesClientBuilder().build().adapt(OpenShiftClient.class)) { Map hard = new HashMap<>(); hard.put("pods", new Quantity("10")); hard.put("secrets", new Quantity("20")); @@ -2788,7 +2810,7 @@ client.quotas().clusterResourceQuotas().withName("foo").delete(); #### ClusterVersion - Fetch Cluster Version: ``` -try (OpenShiftClient client = new DefaultOpenShiftClient()) { +try (OpenShiftClient client = new KubernetesClientBuilder().build().adapt(OpenShiftClient.class)) { ClusterVersion clusterVersion = client.config().clusterVersions().withName("version").get(); System.out.println("Cluster Version: " + clusterVersion.getStatus().getDesired().getVersion()); } @@ -2803,7 +2825,7 @@ EgressNetworkPolicy egressNetworkPolicy = client.egressNetworkPolicies() ``` - Create `EgressNetworkPolicy`: ``` -try (OpenShiftClient client = new DefaultOpenShiftClient()) { +try (OpenShiftClient client = new KubernetesClientBuilder().build().adapt(OpenShiftClient.class)) { EgressNetworkPolicy enp = new EgressNetworkPolicyBuilder() .withNewMetadata() .withName("foo") @@ -2851,19 +2873,12 @@ It is pretty much the same as Kubernetes Client but has support for some additio #### Initializing Tekton Client Initializing Tekton client is the same as Kubernetes Client. You ``` -try (final TektonClient client = new DefaultTektonClient()) { +try (final TektonClient client = new KubernetesClientBuilder().build().adapt(TektonClient.class)) { // Do stuff with client } ``` This would pick up default settings, reading your `kubeconfig` file from `~/.kube/config` directory or whatever is defined inside `KUBECONFIG` environment variable. -But if you want to customize creation of client, you can also pass a `Config` object inside `DefaultTektonClient`. -You can also create `TektonClient` from an existing instance of `KubernetesClient`. -There is a method called `adapt(..)` for this. -Here is an example: -``` -KubernetesClient client = new DefaultKubernetesClient(); -TektonClient tektonClient = client.adapt(TektonClient.class); -``` +But if you want to customize creation of client, you can also pass a `Config` object inside the builder. #### Tekton Client DSL Usage The Tekton client supports CRD API version `tekton.dev/v1alpha1` as well as `tekton.dev/v1beta1`. @@ -2901,19 +2916,12 @@ It is pretty much the same as Kubernetes Client but has support for some additio #### Initializing Knative Client Initializing Knative client is the same as Kubernetes Client. ``` -try (final KnativeClient client = new DefaultKnativeClient()) { +try (final KnativeClient client = new KubernetesClientBuilder().build().adapt(KnativeClient.class)) { // Do stuff with client } ``` This would pick up default settings, reading your `kubeconfig` file from `~/.kube/config` directory or whatever is defined inside `KUBECONFIG` environment variable. -But if you want to customize creation of client, you can also pass a `Config` object inside `DefaultKnativeClient`. -You can also create `KnativeClient` from an existing instance of `KubernetesClient`. -There is a method called `adapt(..)` for this. -Here is an example: -``` -KubernetesClient client = new DefaultKubernetesClient(); -KnativeClient knativeClient = client.adapt(KnativeClient.class); -``` +But if you want to customize creation of client, you can also pass a `Config` object inside the builder. #### Knative Client DSL Usage The usage of the resources follows the same pattern as for K8s resources like Pods or Deployments. diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java index 6d780abee51..167dcd32151 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java @@ -127,7 +127,7 @@ public class Config { public static final String KUBERNETES_PROXY_USERNAME = "proxy.username"; public static final String KUBERNETES_PROXY_PASSWORD = "proxy.password"; - public static final String KUBERNETES_USER_AGENT = "fabric8-kubernetes-client/" + Version.clientVersion(); + public static final String KUBERNETES_USER_AGENT = "kubernetes.user.agent"; public static final String DEFAULT_MASTER_URL = "https://kubernetes.default.svc"; public static final Long DEFAULT_ROLLING_TIMEOUT = 15 * 60 * 1000L; @@ -217,7 +217,7 @@ public class Config { private String proxyUsername; private String proxyPassword; private String[] noProxy; - private String userAgent; + private String userAgent = "fabric8-kubernetes-client/" + Version.clientVersion(); private TlsVersion[] tlsVersions = new TlsVersion[] { TlsVersion.TLS_1_2 }; private Map errorMessages = new HashMap<>(); diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/Patchable.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/Patchable.java index b69c12fd63f..26ebf11ff5b 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/Patchable.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/Patchable.java @@ -23,10 +23,12 @@ public interface Patchable { /** * Update field(s) of a resource using a JSON patch. * - *
It is the same as calling {@link #patch(PatchContext, Object)} with {@link PatchType#JSON} specified. + *
+ * It is the same as calling {@link #patch(PatchContext, Object)} with {@link PatchType#JSON} specified. * - * WARNING: This may overwrite concurrent changes (between when you obtained your item and the current state) in an unexpected way. - * Consider using edit instead. + * WARNING: This may overwrite concurrent changes (between when you obtained your item and the current state) in an unexpected + * way. + * Consider using edit instead or ensure you have called load or withItem to define the base of your patch * * @param item to be patched with patched values * @return returns deserialized version of api server response @@ -39,13 +41,14 @@ default T patch(T item) { * Update field(s) of a resource using type specified in {@link PatchContext}(defaults to strategic merge if not specified). * *
    - *
  • {@link PatchType#JSON} - will create a JSON patch against the current item. - * WARNING: This may overwrite concurrent changes (between when you obtained your item and the current state) in an unexpected way. - * Consider using edit instead. + *
  • {@link PatchType#JSON} - will create a JSON patch against the current item. See the note in {@link #patch(Object)} + * about what is used for the base object. *
  • {@link PatchType#JSON_MERGE} - will send the serialization of the item as a JSON MERGE patch. * Set the resourceVersion to null to prevent optimistic locking. *
  • {@link PatchType#STRATEGIC_MERGE} - will send the serialization of the item as a STRATEGIC MERGE patch. * Set the resourceVersion to null to prevent optimistic locking. + *
  • {@link PatchType#SERVER_SIDE_APPLY} - will send the serialization of the item as a SERVER SIDE APPLY patch. + * You may explicitly set the {@link PatchContext#getFieldManager()} as well to override the default. *
* * @param item to be patched with patched values diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/base/PatchContext.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/base/PatchContext.java index 74324ade357..9fad56035c6 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/base/PatchContext.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/base/PatchContext.java @@ -18,11 +18,13 @@ import java.util.List; public class PatchContext { + // TODO this state overlaps with PatchOptions private List dryRun; private String fieldManager; private Boolean force; private PatchType patchType; - + private String fieldValidation; + public static PatchContext of(PatchType type) { return new PatchContext.Builder().withPatchType(type).build(); } @@ -59,6 +61,14 @@ public void setPatchType(PatchType patchType) { this.patchType = patchType; } + public String getFieldValidation() { + return fieldValidation; + } + + public void setFieldValidation(String fieldValidation) { + this.fieldValidation = fieldValidation; + } + public static class Builder { private final PatchContext patchContext; @@ -86,6 +96,11 @@ public Builder withPatchType(PatchType patchType) { return this; } + public Builder withFieldValidation(String fieldValidation) { + this.patchContext.setFieldValidation(fieldValidation); + return this; + } + public PatchContext build() { return this.patchContext; } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/base/PatchType.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/base/PatchType.java index 0c6ebfb9136..e8b9955ca59 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/base/PatchType.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/base/PatchType.java @@ -21,7 +21,8 @@ public enum PatchType { JSON("application/json-patch+json"), JSON_MERGE("application/merge-patch+json"), - STRATEGIC_MERGE("application/strategic-merge-patch+json"); + STRATEGIC_MERGE("application/strategic-merge-patch+json"), + SERVER_SIDE_APPLY("application/apply-patch+yaml"); private final String contentType; diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java index 089f7d79862..aea85da034b 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java @@ -54,9 +54,11 @@ public class ConfigTest { private static final String TEST_KUBECONFIG_EXEC_FILE = Utils.filePath(ConfigTest.class.getResource("/test-kubeconfig-exec")); private static final String TEST_TOKEN_GENERATOR_FILE = Utils.filePath(ConfigTest.class.getResource("/token-generator")); - private static final String TEST_KUBECONFIG_EXEC_WIN_FILE = Utils.filePath(ConfigTest.class.getResource("/test-kubeconfig-exec-win")); + private static final String TEST_KUBECONFIG_EXEC_WIN_FILE = Utils + .filePath(ConfigTest.class.getResource("/test-kubeconfig-exec-win")); - private static final String TEST_KUBECONFIG_NO_CURRENT_CONTEXT_FILE = Utils.filePath(ConfigTest.class.getResource("/test-kubeconfig-nocurrentctxt.yml")); + private static final String TEST_KUBECONFIG_NO_CURRENT_CONTEXT_FILE = Utils + .filePath(ConfigTest.class.getResource("/test-kubeconfig-nocurrentctxt.yml")); @BeforeEach public void setUp() { @@ -146,41 +148,40 @@ void testWithSystemProperties() { @Test void testWithBuilder() { Config config = new ConfigBuilder() - .withMasterUrl("http://somehost:80") - .withApiVersion("v1") - .withNamespace("testns") - .withOauthToken("token") - .withUsername("user") - .withPassword("pass") - .withTrustCerts(true) - .withDisableHostnameVerification(true) - .withCaCertFile("/path/to/cert") - .withCaCertData("cacertdata") - .withClientCertFile("/path/to/clientcert") - .withClientCertData("clientcertdata") - .withClientKeyFile("/path/to/clientkey") - .withClientKeyData("clientkeydata") - .withClientKeyAlgo("algo") - .withClientKeyPassphrase("passphrase") - .withMaxConcurrentRequests(120) - .withMaxConcurrentRequestsPerHost(20) - .withWatchReconnectInterval(5000) - .withWatchReconnectLimit(5) - .withRequestTimeout(5000) - .withUploadConnectionTimeout(60000) - .withUploadRequestTimeout(600000) - .withHttpProxy("httpProxy") - .withTlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1) - .withTrustStoreFile("/path/to/truststore") - .withTrustStorePassphrase("truststorePassphrase") - .withKeyStoreFile("/path/to/keystore") - .withKeyStorePassphrase("keystorePassphrase") - .build(); + .withMasterUrl("http://somehost:80") + .withApiVersion("v1") + .withNamespace("testns") + .withOauthToken("token") + .withUsername("user") + .withPassword("pass") + .withTrustCerts(true) + .withDisableHostnameVerification(true) + .withCaCertFile("/path/to/cert") + .withCaCertData("cacertdata") + .withClientCertFile("/path/to/clientcert") + .withClientCertData("clientcertdata") + .withClientKeyFile("/path/to/clientkey") + .withClientKeyData("clientkeydata") + .withClientKeyAlgo("algo") + .withClientKeyPassphrase("passphrase") + .withMaxConcurrentRequests(120) + .withMaxConcurrentRequestsPerHost(20) + .withWatchReconnectInterval(5000) + .withWatchReconnectLimit(5) + .withRequestTimeout(5000) + .withUploadConnectionTimeout(60000) + .withUploadRequestTimeout(600000) + .withHttpProxy("httpProxy") + .withTlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1) + .withTrustStoreFile("/path/to/truststore") + .withTrustStorePassphrase("truststorePassphrase") + .withKeyStoreFile("/path/to/keystore") + .withKeyStorePassphrase("keystorePassphrase") + .build(); assertConfig(config); } - @Test void testWithBuilderAndSystemProperties() { System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, "http://tobeoverriden:80"); @@ -218,9 +219,9 @@ void testWithBuilderAndSystemProperties() { System.setProperty(Config.KUBERNETES_UPLOAD_REQUEST_TIMEOUT_SYSTEM_PROPERTY, "600000"); Config config = new ConfigBuilder() - .withMasterUrl("http://somehost:80") - .withNamespace("testns") - .build(); + .withMasterUrl("http://somehost:80") + .withNamespace("testns") + .build(); assertConfig(config); } @@ -307,8 +308,8 @@ void testWithKubeConfigAndSytemPropertiesAndBuilder() { System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, "http://somehost:80"); Config config = new ConfigBuilder() - .withNamespace("testns2") - .build(); + .withNamespace("testns2") + .build(); assertNotNull(config); assertEquals("http://somehost:80/", config.getMasterUrl()); @@ -387,8 +388,8 @@ void testWithNamespacePathAndSytemPropertiesAndBuilder() { System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "tobeoverriden"); Config config = new ConfigBuilder() - .withNamespace("testns2") - .build(); + .withNamespace("testns2") + .build(); assertNotNull(config); assertEquals("http://somehost:80/", config.getMasterUrl()); @@ -397,16 +398,16 @@ void testWithNamespacePathAndSytemPropertiesAndBuilder() { @Test void testWithCustomHeader() { - Map customHeaders = new HashMap<>(); - customHeaders.put("user-id","test-user"); - customHeaders.put("cluster-id","test-cluster"); + Map customHeaders = new HashMap<>(); + customHeaders.put("user-id", "test-user"); + customHeaders.put("cluster-id", "test-cluster"); Config config = new ConfigBuilder() - .withCustomHeaders(customHeaders) - .build(); + .withCustomHeaders(customHeaders) + .build(); assertNotNull(config); assertNotNull(config.getCustomHeaders()); - assertEquals(2,config.getCustomHeaders().size()); + assertEquals(2, config.getCustomHeaders().size()); } @Test @@ -419,12 +420,12 @@ void shouldSetImpersonateUsernameAndGroupFromSystemProperty() { extras.put("c", Collections.singletonList("d")); final Config config = new ConfigBuilder() - .withImpersonateUsername("a") - .withImpersonateExtras(extras) - .build(); + .withImpersonateUsername("a") + .withImpersonateExtras(extras) + .build(); assertEquals("a", config.getImpersonateUsername()); - assertArrayEquals(new String[]{"group"}, config.getImpersonateGroups()); + assertArrayEquals(new String[] { "group" }, config.getImpersonateGroups()); assertEquals(Collections.singletonList("d"), config.getImpersonateExtras().get("c")); } @@ -449,8 +450,8 @@ void honorClientAuthenticatorCommands() throws Exception { void shouldBeUsedTokenSuppliedByProvider() { Config config = new ConfigBuilder().withOauthToken("oauthToken") - .withOauthTokenProvider(() -> "PROVIDER_TOKEN") - .build(); + .withOauthTokenProvider(() -> "PROVIDER_TOKEN") + .build(); assertEquals("PROVIDER_TOKEN", config.getOauthToken()); } @@ -463,13 +464,14 @@ void shouldHonorDefaultWebsocketPingInterval() { } @Test - void testKubeConfigWithAuthConfigProvider() throws URISyntaxException { + void testKubeConfigWithAuthConfigProvider() throws URISyntaxException { System.setProperty("kubeconfig", new File(getClass().getResource("/test-kubeconfig").toURI()).getAbsolutePath()); Config config = Config.autoConfigure("production/172-28-128-4:8443/mmosley"); assertEquals("https://172.28.128.4:8443/", config.getMasterUrl()); - assertEquals("eyJraWQiOiJDTj1vaWRjaWRwLnRyZW1vbG8ubGFuLCBPVT1EZW1vLCBPPVRybWVvbG8gU2VjdXJpdHksIEw9QXJsaW5ndG9uLCBTVD1WaXJnaW5pYSwgQz1VUy1DTj1rdWJlLWNhLTEyMDIxNDc5MjEwMzYwNzMyMTUyIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL29pZGNpZHAudHJlbW9sby5sYW46ODQ0My9hdXRoL2lkcC9PaWRjSWRQIiwiYXVkIjoia3ViZXJuZXRlcyIsImV4cCI6MTQ4MzU0OTUxMSwianRpIjoiMm96US15TXdFcHV4WDlHZUhQdy1hZyIsImlhdCI6MTQ4MzU0OTQ1MSwibmJmIjoxNDgzNTQ5MzMxLCJzdWIiOiI0YWViMzdiYS1iNjQ1LTQ4ZmQtYWIzMC0xYTAxZWU0MWUyMTgifQ.w6p4J_6qQ1HzTG9nrEOrubxIMb9K5hzcMPxc9IxPx2K4xO9l-oFiUw93daH3m5pluP6K7eOE6txBuRVfEcpJSwlelsOsW8gb8VJcnzMS9EnZpeA0tW_p-mnkFc3VcfyXuhe5R3G7aa5d8uHv70yJ9Y3-UhjiN9EhpMdfPAoEB9fYKKkJRzF7utTTIPGrSaSU6d2pcpfYKaxIwePzEkT4DfcQthoZdy9ucNvvLoi1DIC-UocFD8HLs8LYKEqSxQvOcvnThbObJ9af71EwmuE21fO5KzMW20KtAeget1gnldOosPtz1G5EwvaQ401-RPQzPGMVBld0_zMCAwZttJ4knw", - config.getOauthToken()); + assertEquals( + "eyJraWQiOiJDTj1vaWRjaWRwLnRyZW1vbG8ubGFuLCBPVT1EZW1vLCBPPVRybWVvbG8gU2VjdXJpdHksIEw9QXJsaW5ndG9uLCBTVD1WaXJnaW5pYSwgQz1VUy1DTj1rdWJlLWNhLTEyMDIxNDc5MjEwMzYwNzMyMTUyIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL29pZGNpZHAudHJlbW9sby5sYW46ODQ0My9hdXRoL2lkcC9PaWRjSWRQIiwiYXVkIjoia3ViZXJuZXRlcyIsImV4cCI6MTQ4MzU0OTUxMSwianRpIjoiMm96US15TXdFcHV4WDlHZUhQdy1hZyIsImlhdCI6MTQ4MzU0OTQ1MSwibmJmIjoxNDgzNTQ5MzMxLCJzdWIiOiI0YWViMzdiYS1iNjQ1LTQ4ZmQtYWIzMC0xYTAxZWU0MWUyMTgifQ.w6p4J_6qQ1HzTG9nrEOrubxIMb9K5hzcMPxc9IxPx2K4xO9l-oFiUw93daH3m5pluP6K7eOE6txBuRVfEcpJSwlelsOsW8gb8VJcnzMS9EnZpeA0tW_p-mnkFc3VcfyXuhe5R3G7aa5d8uHv70yJ9Y3-UhjiN9EhpMdfPAoEB9fYKKkJRzF7utTTIPGrSaSU6d2pcpfYKaxIwePzEkT4DfcQthoZdy9ucNvvLoi1DIC-UocFD8HLs8LYKEqSxQvOcvnThbObJ9af71EwmuE21fO5KzMW20KtAeget1gnldOosPtz1G5EwvaQ401-RPQzPGMVBld0_zMCAwZttJ4knw", + config.getOauthToken()); } @Test @@ -507,6 +509,7 @@ void testEmptyConfig() { assertFalse(emptyConfig.isHttp2Disable()); assertEquals(1, emptyConfig.getTlsVersions().length); assertTrue(emptyConfig.getErrorMessages().isEmpty()); + assertNotNull(emptyConfig.getUserAgent()); } private void assertConfig(Config config) { @@ -536,7 +539,7 @@ private void assertConfig(Config config) { assertEquals(60000, config.getRequestConfig().getUploadConnectionTimeout()); assertEquals(600000, config.getRequestConfig().getUploadRequestTimeout()); - assertArrayEquals(new TlsVersion[]{TlsVersion.TLS_1_2, TlsVersion.TLS_1_1}, config.getTlsVersions()); + assertArrayEquals(new TlsVersion[] { TlsVersion.TLS_1_2, TlsVersion.TLS_1_1 }, config.getTlsVersions()); assertEquals("/path/to/truststore", config.getTrustStoreFile()); assertEquals("truststorePassphrase", config.getTrustStorePassphrase()); @@ -555,13 +558,14 @@ void testGetAuthenticatorCommandFromExecConfig() throws IOException { boolean isNewFileCreated = commandFile.createNewFile(); String systemPathValue = getTestPathValue(commandFolder); ExecConfig execConfig = new ExecConfigBuilder() - .withApiVersion("client.authentication.k8s.io/v1alpha1") - .addToArgs("--region", "us-west2", "eks", "get-token", "--cluster-name", "api-eks.example.com") - .withCommand("aws") - .build(); + .withApiVersion("client.authentication.k8s.io/v1alpha1") + .addToArgs("--region", "us-west2", "eks", "get-token", "--cluster-name", "api-eks.example.com") + .withCommand("aws") + .build(); // When - List processBuilderArgs = Config.getAuthenticatorCommandFromExecConfig(execConfig, new File("~/.kube/config"), systemPathValue); + List processBuilderArgs = Config.getAuthenticatorCommandFromExecConfig(execConfig, new File("~/.kube/config"), + systemPathValue); // Then assertTrue(isNewFileCreated); @@ -587,12 +591,12 @@ private void assertPlatformPrefixes(List processBuilderArgs) { private String getTestPathValue(File commandFolder) { if (Utils.isWindowsOperatingSystem()) { return "C:\\Program Files\\Java\\jdk14.0_23\\bin" + File.pathSeparator + - commandFolder.getAbsolutePath() + File.pathSeparator + - "C:\\Program Files\\Apache Software Foundation\\apache-maven-3.3.1"; + commandFolder.getAbsolutePath() + File.pathSeparator + + "C:\\Program Files\\Apache Software Foundation\\apache-maven-3.3.1"; } else { return "/usr/java/jdk-14.0.1/bin" + File.pathSeparator + - commandFolder.getAbsolutePath() + File.pathSeparator + - "/opt/apache-maven/bin"; + commandFolder.getAbsolutePath() + File.pathSeparator + + "/opt/apache-maven/bin"; } } } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java index e39c40e9d27..1e5fdf911d6 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java @@ -61,7 +61,7 @@ public class OperationSupport { public static final String JSON_PATCH = "application/json-patch+json"; public static final String STRATEGIC_MERGE_JSON_PATCH = "application/strategic-merge-patch+json"; public static final String JSON_MERGE_PATCH = "application/merge-patch+json"; - + protected static final ObjectMapper JSON_MAPPER = Serialization.jsonMapper(); private static final Logger LOG = LoggerFactory.getLogger(OperationSupport.class); private static final String CLIENT_STATUS_FLAG = "CLIENT_STATUS_FLAG"; @@ -80,7 +80,7 @@ public class OperationSupport { private final int requestRetryBackoffLimit; public OperationSupport() { - this (new OperationContext()); + this(new OperationContext()); } public OperationSupport(HttpClient client, Config config) { @@ -93,7 +93,7 @@ public OperationSupport(OperationContext ctx) { this.config = ctx.getConfig(); this.resourceT = ctx.getPlural(); this.namespace = ctx.getNamespace(); - this.name = ctx.getName() ; + this.name = ctx.getName(); this.apiGroupName = ctx.getApiGroupName(); this.dryRun = ctx.getDryRun(); if (Utils.isNotNullOrEmpty(ctx.getApiGroupVersion())) { @@ -112,7 +112,8 @@ public OperationSupport(OperationContext ctx) { requestRetryBackoffInterval = Config.DEFAULT_REQUEST_RETRY_BACKOFFINTERVAL; this.requestRetryBackoffLimit = Config.DEFAULT_REQUEST_RETRY_BACKOFFLIMIT; } - this.retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(requestRetryBackoffInterval, maxRetryIntervalExponent); + this.retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(requestRetryBackoffInterval, + maxRetryIntervalExponent); } public String getAPIGroupName() { @@ -168,7 +169,7 @@ public URL getNamespacedUrl() throws MalformedURLException { public URL getResourceUrl(String namespace, String name) throws MalformedURLException { return getResourceUrl(namespace, name, false); } - + public URL getResourceUrl(String namespace, String name, boolean status) throws MalformedURLException { if (name == null) { if (status) { @@ -205,23 +206,30 @@ public URL getResourceURLForPatchOperation(URL resourceUrl, PatchContext patchCo if ((patchContext.getDryRun() != null && !patchContext.getDryRun().isEmpty()) || dryRun) { url = URLUtils.join(url, "?dryRun=All"); } - if (patchContext.getFieldManager() != null) { - url = URLUtils.join(url, "?fieldManager=" + patchContext.getFieldManager()); + String fieldManager = patchContext.getFieldManager(); + if (fieldManager == null && patchContext.getPatchType() == PatchType.SERVER_SIDE_APPLY) { + fieldManager = "fabric8"; + } + if (fieldManager != null) { + url = URLUtils.join(url, "?fieldManager=" + fieldManager); + } + if (patchContext.getFieldValidation() != null) { + url = URLUtils.join(url, "?fieldValidation=" + patchContext.getFieldValidation()); } return new URL(url); } return resourceUrl; } - + protected T correctNamespace(T item) { if (!isResourceNamespaced() || this.context.isDefaultNamespace() || !(item instanceof HasMetadata)) { return item; } - String itemNs = KubernetesResourceUtil.getNamespace((HasMetadata)item); - + String itemNs = KubernetesResourceUtil.getNamespace((HasMetadata) item); + if (Utils.isNotNullOrEmpty(namespace) && Utils.isNotNullOrEmpty(itemNs) && !namespace.equals(itemNs)) { item = Serialization.clone(item); - KubernetesResourceUtil.setNamespace((HasMetadata)item, namespace); + KubernetesResourceUtil.setNamespace((HasMetadata) item, namespace); } return item; } @@ -231,7 +239,7 @@ protected String checkNamespace(T item) { return null; } String operationNs = getNamespace(); - String itemNs = (item instanceof HasMetadata) ? KubernetesResourceUtil.getNamespace((HasMetadata)item) : null; + String itemNs = (item instanceof HasMetadata) ? KubernetesResourceUtil.getNamespace((HasMetadata) item) : null; if (Utils.isNullOrEmpty(operationNs) && Utils.isNullOrEmpty(itemNs)) { throw new KubernetesClientException("namespace not specified for an operation requiring one."); } else if (!Utils.isNullOrEmpty(itemNs) && (Utils.isNullOrEmpty(operationNs) @@ -258,16 +266,19 @@ protected String checkName(T item) { } protected T handleMetric(String resourceUrl, Class type) throws InterruptedException, IOException { - HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder() + HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder() .uri(resourceUrl); - return handleResponse(requestBuilder, type); + return handleResponse(requestBuilder, type); } - protected void handleDelete(T resource, long gracePeriodSeconds, DeletionPropagation propagationPolicy, String resourceVersion, boolean cascading) throws InterruptedException, IOException { - handleDelete(getResourceURLForWriteOperation(getResourceUrl(checkNamespace(resource), checkName(resource))), gracePeriodSeconds, propagationPolicy, resourceVersion, cascading); + protected void handleDelete(T resource, long gracePeriodSeconds, DeletionPropagation propagationPolicy, + String resourceVersion, boolean cascading) throws InterruptedException, IOException { + handleDelete(getResourceURLForWriteOperation(getResourceUrl(checkNamespace(resource), checkName(resource))), + gracePeriodSeconds, propagationPolicy, resourceVersion, cascading); } - protected void handleDelete(URL requestUrl, long gracePeriodSeconds, DeletionPropagation propagationPolicy, String resourceVersion, boolean cascading) throws InterruptedException, IOException { + protected void handleDelete(URL requestUrl, long gracePeriodSeconds, DeletionPropagation propagationPolicy, + String resourceVersion, boolean cascading) throws InterruptedException, IOException { DeleteOptions deleteOptions = new DeleteOptions(); if (gracePeriodSeconds >= 0) { deleteOptions.setGracePeriodSeconds(gracePeriodSeconds); @@ -288,11 +299,11 @@ protected void handleDelete(URL requestUrl, long gracePeriodSeconds, DeletionPro deleteOptions.setDryRun(Collections.singletonList("All")); } - HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder().delete(JSON, JSON_MAPPER.writeValueAsString(deleteOptions)).url(requestUrl); - handleResponse(requestBuilder, null, Collections.emptyMap()); + HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder() + .delete(JSON, JSON_MAPPER.writeValueAsString(deleteOptions)).url(requestUrl); + handleResponse(requestBuilder, null, Collections. emptyMap()); } - /** * Create a resource. * @@ -310,10 +321,9 @@ protected T handleCreate(I resource, Class outputType) throws Interrup HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder() .post(JSON, JSON_MAPPER.writeValueAsString(resource)) .url(getResourceURLForWriteOperation(getResourceUrl(checkNamespace(resource), null))); - return handleResponse(requestBuilder, outputType, Collections.emptyMap()); + return handleResponse(requestBuilder, outputType, Collections. emptyMap()); } - /** * Replace a resource. * @@ -327,7 +337,7 @@ protected T handleCreate(I resource, Class outputType) throws Interrup * @throws IOException IOException */ protected T handleUpdate(T updated, Class type, boolean status) throws InterruptedException, IOException { - return handleUpdate(updated, type, Collections.emptyMap(), status); + return handleUpdate(updated, type, Collections. emptyMap(), status); } /** @@ -343,7 +353,8 @@ protected T handleUpdate(T updated, Class type, boolean status) throws In * @throws InterruptedException Interrupted Exception * @throws IOException IOException */ - protected T handleUpdate(T updated, Class type, Map parameters, boolean status) throws InterruptedException, IOException { + protected T handleUpdate(T updated, Class type, Map parameters, boolean status) + throws InterruptedException, IOException { updated = correctNamespace(updated); HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder() .put(JSON, JSON_MAPPER.writeValueAsString(updated)) @@ -354,7 +365,8 @@ protected T handleUpdate(T updated, Class type, Map param /** * Send an http patch and handle the response. * - * If current is not null and patchContext does not specify a patch type, then a JSON patch is assumed. Otherwise a STRATEGIC MERGE is assumed. + * If current is not null and patchContext does not specify a patch type, then a JSON patch is assumed. Otherwise a STRATEGIC + * MERGE is assumed. * * @param patchContext patch options for patch request * @param current current object @@ -367,14 +379,27 @@ protected T handleUpdate(T updated, Class type, Map param * @throws InterruptedException Interrupted Exception * @throws IOException IOException */ - protected T handlePatch(PatchContext patchContext, T current, T updated, Class type, boolean status) throws InterruptedException, IOException { + protected T handlePatch(PatchContext patchContext, T current, T updated, Class type, boolean status) + throws InterruptedException, IOException { String patchForUpdate = null; if (current != null && (patchContext == null || patchContext.getPatchType() == PatchType.JSON)) { - patchForUpdate = JSON_MAPPER.writeValueAsString(JsonDiff.asJson(patchMapper().valueToTree(current), patchMapper().valueToTree(updated))); + patchForUpdate = JSON_MAPPER + .writeValueAsString(JsonDiff.asJson(patchMapper().valueToTree(current), patchMapper().valueToTree(updated))); if (patchContext == null) { patchContext = new PatchContext.Builder().withPatchType(PatchType.JSON).build(); } } else { + if (patchContext != null + && patchContext.getPatchType() == PatchType.SERVER_SIDE_APPLY) { + // TODO: it would probably be better to do this with a mixin + if (updated instanceof HasMetadata) { + ObjectMeta meta = ((HasMetadata) updated).getMetadata(); + if (meta != null && meta.getManagedFields() != null && !meta.getManagedFields().isEmpty()) { + // the item should have already been cloned + meta.setManagedFields(null); + } + } + } patchForUpdate = Serialization.asJson(updated); current = updated; // use the updated to determine the path } @@ -393,7 +418,8 @@ protected T handlePatch(PatchContext patchContext, T current, T updated, Cla * @throws InterruptedException Interrupted Exception * @throws IOException IOException */ - protected T handlePatch(T current, Map patchForUpdate, Class type) throws InterruptedException, IOException { + protected T handlePatch(T current, Map patchForUpdate, Class type) + throws InterruptedException, IOException { return handlePatch(new PatchContext.Builder().withPatchType(PatchType.STRATEGIC_MERGE).build(), current, JSON_MAPPER.writeValueAsString(patchForUpdate), type, false); } @@ -411,7 +437,8 @@ protected T handlePatch(T current, Map patchForUpdate, Class * @throws InterruptedException Interrupted Exception * @throws IOException IOException in case of network errors */ - protected T handlePatch(PatchContext patchContext, T current, String patchForUpdate, Class type, boolean status) throws InterruptedException, IOException { + protected T handlePatch(PatchContext patchContext, T current, String patchForUpdate, Class type, boolean status) + throws InterruptedException, IOException { String bodyContentType = getContentTypeFromPatchContextOrDefault(patchContext); HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder() .patch(bodyContentType, patchForUpdate) @@ -446,8 +473,10 @@ protected Scale handleScale(String resourceUrl, Scale scale) throws InterruptedE * @throws InterruptedException in case thread is interrupted * @throws IOException in some other I/O problem */ - protected Status handleDeploymentRollback(String resourceUrl, DeploymentRollback deploymentRollback) throws InterruptedException, IOException { - HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder().uri(resourceUrl + "/rollback").post(JSON, JSON_MAPPER.writeValueAsString(deploymentRollback)); + protected Status handleDeploymentRollback(String resourceUrl, DeploymentRollback deploymentRollback) + throws InterruptedException, IOException { + HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder().uri(resourceUrl + "/rollback").post(JSON, + JSON_MAPPER.writeValueAsString(deploymentRollback)); return handleResponse(requestBuilder, Status.class); } @@ -463,15 +492,15 @@ protected Status handleDeploymentRollback(String resourceUrl, DeploymentRollback * @throws IOException IOException */ protected T handleGet(URL resourceUrl, Class type) throws InterruptedException, IOException { - return handleGet(resourceUrl, type, Collections.emptyMap()); + return handleGet(resourceUrl, type, Collections. emptyMap()); } - + /** * Send a raw get - where the type should be one of String, Reader, InputStream *
* NOTE: Currently does not utilize the retry logic */ - protected T handleRawGet(URL resourceUrl, Class type) throws IOException{ + protected T handleRawGet(URL resourceUrl, Class type) throws IOException { HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder().url(resourceUrl); HttpRequest request = requestBuilder.build(); HttpResponse response = httpClient.send(request, type); @@ -491,7 +520,8 @@ protected T handleRawGet(URL resourceUrl, Class type) throws IOException{ * @throws InterruptedException Interrupted Exception * @throws IOException IOException */ - protected T handleGet(URL resourceUrl, Class type, Map parameters) throws InterruptedException, IOException { + protected T handleGet(URL resourceUrl, Class type, Map parameters) + throws InterruptedException, IOException { HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder().url(resourceUrl); return handleResponse(requestBuilder, type, parameters); } @@ -508,7 +538,7 @@ protected T handleGet(URL resourceUrl, Class type, Map pa * @throws IOException IOException */ protected T handleResponse(HttpRequest.Builder requestBuilder, Class type) throws InterruptedException, IOException { - return handleResponse(requestBuilder, type, Collections.emptyMap()); + return handleResponse(requestBuilder, type, Collections. emptyMap()); } /** @@ -523,7 +553,8 @@ protected T handleResponse(HttpRequest.Builder requestBuilder, Class type * @throws InterruptedException Interrupted Exception * @throws IOException IOException */ - protected T handleResponse(HttpRequest.Builder requestBuilder, Class type, Map parameters) throws InterruptedException, IOException { + protected T handleResponse(HttpRequest.Builder requestBuilder, Class type, Map parameters) + throws InterruptedException, IOException { return handleResponse(httpClient, requestBuilder, type, parameters); } @@ -539,24 +570,26 @@ protected T handleResponse(HttpRequest.Builder requestBuilder, Class type * @throws InterruptedException Interrupted Exception * @throws IOException IOException */ - protected T handleResponse(HttpClient client, HttpRequest.Builder requestBuilder, Class type) throws InterruptedException, IOException { - return handleResponse(client, requestBuilder, type, Collections.emptyMap()); + protected T handleResponse(HttpClient client, HttpRequest.Builder requestBuilder, Class type) + throws InterruptedException, IOException { + return handleResponse(client, requestBuilder, type, Collections. emptyMap()); } /** * Send an http request and handle the response, optionally performing placeholder substitution to the response. * - * @param client the client - * @param requestBuilder Request builder - * @param type Type of object provided - * @param parameters A hashmap containing parameters - * @param Template argument provided + * @param client the client + * @param requestBuilder Request builder + * @param type Type of object provided + * @param parameters A hashmap containing parameters + * @param Template argument provided * - * @return Returns a de-serialized object as api server response of provided type. + * @return Returns a de-serialized object as api server response of provided type. * @throws InterruptedException Interrupted Exception * @throws IOException IOException */ - protected T handleResponse(HttpClient client, HttpRequest.Builder requestBuilder, Class type, Map parameters) throws InterruptedException, IOException { + protected T handleResponse(HttpClient client, HttpRequest.Builder requestBuilder, Class type, + Map parameters) throws InterruptedException, IOException { VersionUsageUtils.log(this.resourceT, this.apiGroupVersion); HttpRequest request = requestBuilder.build(); HttpResponse response = retryWithExponentialBackoff(client, request); @@ -575,7 +608,8 @@ protected T handleResponse(HttpClient client, HttpRequest.Builder requestBui } } - protected HttpResponse retryWithExponentialBackoff(HttpClient client, HttpRequest request) throws InterruptedException, IOException { + protected HttpResponse retryWithExponentialBackoff(HttpClient client, HttpRequest request) + throws InterruptedException, IOException { int numRetries = 0; long retryInterval; while (true) { @@ -583,7 +617,8 @@ protected HttpResponse retryWithExponentialBackoff(HttpClient clien HttpResponse response = client.send(request, InputStream.class); if (numRetries < requestRetryBackoffLimit && response.code() >= 500) { retryInterval = retryIntervalCalculator.getInterval(numRetries); - LOG.debug("HTTP operation on url: {} should be retried as the response code was {}, retrying after {} millis", request.uri(), response.code(), retryInterval); + LOG.debug("HTTP operation on url: {} should be retried as the response code was {}, retrying after {} millis", + request.uri(), response.code(), retryInterval); if (response.body() != null) { response.body().close(); } @@ -593,7 +628,8 @@ protected HttpResponse retryWithExponentialBackoff(HttpClient clien } catch (IOException ie) { if (numRetries < requestRetryBackoffLimit) { retryInterval = retryIntervalCalculator.getInterval(numRetries); - LOG.debug(String.format("HTTP operation on url: %s should be retried after %d millis because of IOException", request.uri(), retryInterval), ie); + LOG.debug(String.format("HTTP operation on url: %s should be retried after %d millis because of IOException", + request.uri(), retryInterval), ie); } else { throw ie; } @@ -606,8 +642,8 @@ protected HttpResponse retryWithExponentialBackoff(HttpClient clien /** * Checks if the response status code is the expected and throws the appropriate KubernetesClientException if not. * - * @param request The {#link HttpRequest} object. - * @param response The {@link HttpResponse} object. + * @param request The {#link HttpRequest} object. + * @param response The {@link HttpResponse} object. */ protected void assertResponseCode(HttpRequest request, HttpResponse response) { int statusCode = response.code(); @@ -637,7 +673,7 @@ public static Status createStatus(HttpResponse response) { int statusCode = response != null ? response.code() : 0; if (response == null) { statusMessage = "No response"; - } else { + } else { try { String bodyString = response.bodyString(); if (Utils.isNotNullOrEmpty(bodyString)) { @@ -659,9 +695,9 @@ public static Status createStatus(HttpResponse response) { public static Status createStatus(int statusCode, String message) { Status status = new StatusBuilder() - .withCode(statusCode) - .withMessage(message) - .build(); + .withCode(statusCode) + .withMessage(message) + .build(); status.getAdditionalProperties().put(CLIENT_STATUS_FLAG, "true"); return status; } @@ -672,12 +708,12 @@ public static KubernetesClientException requestFailure(HttpRequest request, Stat public static KubernetesClientException requestFailure(HttpRequest request, Status status, String message) { StringBuilder sb = new StringBuilder(); - if(message != null && !message.isEmpty()) { + if (message != null && !message.isEmpty()) { sb.append(message).append(". "); } - + sb.append("Failure executing: ").append(request.method()) - .append(" at: ").append(request.uri()).append("."); + .append(" at: ").append(request.uri()).append("."); if (status.getMessage() != null && !status.getMessage().isEmpty()) { sb.append(" Message: ").append(status.getMessage()).append("."); @@ -688,7 +724,8 @@ public static KubernetesClientException requestFailure(HttpRequest request, Stat } final RequestMetadata metadata = RequestMetadata.from(request); - return new KubernetesClientException(sb.toString(), status.getCode(), status, metadata.group, metadata.version, metadata.plural, metadata.namespace); + return new KubernetesClientException(sb.toString(), status.getCode(), status, metadata.group, metadata.version, + metadata.plural, metadata.namespace); } public static KubernetesClientException requestException(HttpRequest request, Throwable e, String message) { @@ -696,19 +733,20 @@ public static KubernetesClientException requestException(HttpRequest request, Th if (message != null && !message.isEmpty()) { sb.append(message).append(". "); } - + sb.append("Error executing: ").append(request.method()) - .append(" at: ").append(request.uri()) - .append(". Cause: ").append(e.getMessage()); + .append(" at: ").append(request.uri()) + .append(". Cause: ").append(e.getMessage()); final RequestMetadata metadata = RequestMetadata.from(request); - return new KubernetesClientException(sb.toString(), e, metadata.group, metadata.version, metadata.plural, metadata.namespace); + return new KubernetesClientException(sb.toString(), e, metadata.group, metadata.version, metadata.plural, + metadata.namespace); } public static KubernetesClientException requestException(HttpRequest request, Exception e) { return requestException(request, e, null); } - + private static class RequestMetadata { private final String group; private final String version; @@ -747,7 +785,7 @@ protected static T unmarshal(InputStream is, final Class type) { } protected static T unmarshal(InputStream is, TypeReference type) { - return Serialization.unmarshal(is, type); + return Serialization.unmarshal(is, type); } protected static Map getObjectValueAsMap(T object) { @@ -764,7 +802,7 @@ private String getContentTypeFromPatchContextOrDefault(PatchContext patchContext } return STRATEGIC_MERGE_JSON_PATCH; } - + public R1 restCall(Class result, String... path) { try { URL requestUrl = new URL(config.getMasterUrl()); @@ -785,6 +823,6 @@ public R1 restCall(Class result, String... path) { } catch (IOException e) { throw KubernetesClientException.launderThrowable(e); } - } - + } + } diff --git a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ServerSideApplyIT.java b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ServerSideApplyIT.java new file mode 100644 index 00000000000..31d74341c85 --- /dev/null +++ b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ServerSideApplyIT.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.kubernetes; + +import io.fabric8.commons.AssumingK8sVersionAtLeast; +import io.fabric8.kubernetes.api.model.IntOrString; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import org.arquillian.cube.kubernetes.api.Session; +import org.arquillian.cube.kubernetes.impl.requirement.RequiresKubernetes; +import org.arquillian.cube.requirement.ArquillianConditionalRunner; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(ArquillianConditionalRunner.class) +@RequiresKubernetes +public class ServerSideApplyIT { + + @ClassRule + public static final AssumingK8sVersionAtLeast assumingK8sVersion = + new AssumingK8sVersionAtLeast("1", "22"); + + @ArquillianResource + KubernetesClient client; + + @ArquillianResource + Session session; + + @Test + public void testServerSideApply() { + Service service = new ServiceBuilder() + .withNewMetadata().withName(PatchIT.class.getSimpleName().toLowerCase() + "-svc").endMetadata() + .withNewSpec() + .addToSelector("app", "testapp") + .addNewPort() + .withProtocol("TCP") + .withPort(80) + .withTargetPort(new IntOrString(9376)) + .endPort() + .endSpec() + .build(); + + Resource resource = client.services().inNamespace(session.getNamespace()).withItem(service); + resource.delete(); + + // 1st apply - create must be a server side apply - otherwise the later operations will need to force + service = resource.patch(PatchContext.of(PatchType.SERVER_SIDE_APPLY), service); + assertNotNull(service); + assertEquals(1, service.getSpec().getPorts().size()); + // make sure a cluster ip was assigned + String clusterIp = service.getSpec().getClusterIP(); + assertNotNull(clusterIp); + + // Modify resource + service.getSpec().setSelector(Collections.singletonMap("app", "other")); + + // 2nd server side apply + service = resource.patch(PatchContext.of(PatchType.SERVER_SIDE_APPLY), service); + assertEquals("other", service.getSpec().getSelector().get("app")); + assertEquals(clusterIp, service.getSpec().getClusterIP()); + } + +}