Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #3334: adding basic support for server side apply #3937

Merged
merged 5 commits into from
Mar 12, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
94 changes: 51 additions & 43 deletions doc/CHEATSHEET.md
Original file line number Diff line number Diff line change
@@ -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<CronTab>() {
```

### 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<String, Quantity> 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.
Original file line number Diff line number Diff line change
@@ -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<Integer, String> errorMessages = new HashMap<>();
Original file line number Diff line number Diff line change
@@ -23,10 +23,12 @@ public interface Patchable<T> {
/**
* Update field(s) of a resource using a JSON patch.
*
* <br>It is the same as calling {@link #patch(PatchContext, Object)} with {@link PatchType#JSON} specified.
* <br>
* 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).
*
* <ul>
* <li>{@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.
* <li>{@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.
* <li>{@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.
* <li>{@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.
* <li>{@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.
* </ul>
*
* @param item to be patched with patched values
Original file line number Diff line number Diff line change
@@ -18,11 +18,13 @@
import java.util.List;

public class PatchContext {
// TODO this state overlaps with PatchOptions
private List<String> 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;
}
Original file line number Diff line number Diff line change
@@ -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;

Original file line number Diff line number Diff line change
@@ -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<String,String> customHeaders = new HashMap<>();
customHeaders.put("user-id","test-user");
customHeaders.put("cluster-id","test-cluster");
Map<String, String> 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<String> processBuilderArgs = Config.getAuthenticatorCommandFromExecConfig(execConfig, new File("~/.kube/config"), systemPathValue);
List<String> processBuilderArgs = Config.getAuthenticatorCommandFromExecConfig(execConfig, new File("~/.kube/config"),
systemPathValue);

// Then
assertTrue(isNewFileCreated);
@@ -587,12 +591,12 @@ private void assertPlatformPrefixes(List<String> 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";
}
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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<Service> 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());
}

}