Skip to content

Commit

Permalink
feat(kubernetes-client-api): add pod ephemeral container operations
Browse files Browse the repository at this point in the history
Add new interface EphemeralContainersResource to expose operations on this
new operation context and is returned by a new method on PodResource.

OperationContext and OperationSupport have been updated to support implementing operations on named resource sub-resources.

Fixes #4758
  • Loading branch information
cronik authored and manusa committed Feb 13, 2023
1 parent cfe295b commit ecafb22
Show file tree
Hide file tree
Showing 14 changed files with 698 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* Fix #4804: Update CertManager Model to v1.11.0

#### New Features
* Fix #4758: added support for pod ephemeral container operations

#### _**Note**_: Breaking changes

Expand Down
30 changes: 30 additions & 0 deletions doc/CHEATSHEET.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,36 @@ deleteLatch.await(10, TimeUnit.MINUTES)
String result = new BufferedReader(new InputStreamReader(is)).lines().collect(Collectors.joining("\n"));
}
```
- Add ephemeral container to a `Pod`
```java
PodResource resource = client.pods().withName("pod1");
resource.ephemeralContainers()
.edit(p -> new PodBuilder(p)
.editSpec()
.addNewEphemeralContainer()
.withName("debugger")
.withImage("busybox")
.withCommand("sleep", "36000")
.endEphemeralContainer()
.endSpec()
.build());

resource.waitUntilCondition(p -> {
return p.getStatus()
.getEphemeralContainerStatuses()
.stream()
.filter(s -> s.getName().equals("debugger"))
.anyMatch(s -> s.getState().getRunning() != null);
}, 2, TimeUnit.MINUTES);

ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ExecWatch watch = resource.inContainer("debugger")
.writingOutput(out)
.exec("sh", "-c", "echo 'hello world!'")) {
assertEquals(0, watch.exitCode().join());
assertEquals("hello world!\n", out.toString());
}
```
- Using Kubernetes Client from within a `Pod`
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:
```
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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.client.server.mock;

import io.fabric8.kubernetes.api.model.Status;
import io.fabric8.kubernetes.api.model.StatusBuilder;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.fabric8.mockwebserver.internal.WebSocketMessage;

import java.nio.charset.StandardCharsets;

/**
* {@link WebSocketMessage} that outputs a {@link Status} message.
*/
public class StatusMessage extends WebSocketMessage {

private static final byte ERROR_CHANNEL_STREAM_ID = 3;

/**
* Create websocket message that returns {@link Status} on the error channel.
*
* @param status status response
*/
public StatusMessage(Status status) {
super(0L, getBodyBytes(ERROR_CHANNEL_STREAM_ID, status), true, true);
}

/**
* Create websocket message that returns a {@link Status} with the given status code
* on the error channel.
*
* @param exitCode exit code
*/
public StatusMessage(int exitCode) {
this(new StatusBuilder()
.withCode(exitCode)
.withStatus(exitCode == 0 ? "Success" : "Failure")
.build());
}

private static byte[] getBodyBytes(byte prefix, Status status) {
byte[] original = Serialization.asJson(status).getBytes(StandardCharsets.UTF_8);
byte[] prefixed = new byte[original.length + 1];
prefixed[0] = prefix;
System.arraycopy(original, 0, prefixed, 1, original.length);
return prefixed;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* 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.client.server.mock;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class StatusMessageTest {

@Test
void exitCode() {
assertEquals("\u0003{\"apiVersion\":\"v1\",\"kind\":\"Status\",\"code\":0,\"status\":\"Success\"}",
new StatusMessage(0).getBody());
assertEquals("\u0003{\"apiVersion\":\"v1\",\"kind\":\"Status\",\"code\":1,\"status\":\"Failure\"}",
new StatusMessage(1).getBody());
assertEquals("\u0003{\"apiVersion\":\"v1\",\"kind\":\"Status\",\"code\":-1,\"status\":\"Failure\"}",
new StatusMessage(-1).getBody());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* 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.client.dsl;

import io.fabric8.kubernetes.api.model.Pod;

/**
* Operations for Pod Ephemeral Containers. Ephemeral containers are a special type of container that runs temporarily
* in an existing Pod to accomplish user-initiated actions such as troubleshooting. You use ephemeral containers to
* inspect services rather than to build applications.
*
* @see <a href="https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/">About Ephemeral Containers</a>
* @see <a href=
* "hhttps://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#-strong-ephemeralcontainers-operations-pod-v1-core-strong-">Ephemeral
* Containers Operations</a>
*/
public interface EphemeralContainersResource extends EditReplacePatchable<Pod> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public interface PodResource extends Resource<Pod>,
Loggable,
Containerable<String, ContainerResource>,
ContainerResource,
EphemeralContainersResource,
PortForwardable {

/**
Expand All @@ -40,4 +41,12 @@ public interface PodResource extends Resource<Pod>,
* @throws io.fabric8.kubernetes.client.KubernetesClientException if an error occurs, including if the Pod is not found.
*/
boolean evict(Eviction eviction);

/**
* Manage ephemeral containers for a pod.
*
* @return ephemeral containers resource operations
*/
EphemeralContainersResource ephemeralContainers();

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class OperationContext {
protected String namespace;
protected boolean defaultNamespace = true;
protected String name;
protected String subresource;
protected boolean dryRun;
protected FieldValidateable.Validation fieldValidation;
protected String fieldManager;
Expand All @@ -75,14 +76,15 @@ public OperationContext() {
}

public OperationContext(OperationContext other) {
this(other.client, other.plural, other.namespace, other.name, other.apiGroupName, other.apiGroupVersion,
this(other.client, other.plural, other.namespace, other.name, other.subresource, other.apiGroupName, other.apiGroupVersion,
other.item, other.labels, other.labelsNot, other.labelsIn, other.labelsNotIn, other.fields,
other.fieldsNot, other.resourceVersion, other.gracePeriodSeconds, other.propagationPolicy,
other.dryRun, other.selectorAsString, other.defaultNamespace, other.fieldValidation, other.fieldManager,
other.forceConflicts, other.timeout, other.timeoutUnit);
}

public OperationContext(Client client, String plural, String namespace, String name,
@SuppressWarnings("java:S107")
public OperationContext(Client client, String plural, String namespace, String name, String subresource,
String apiGroupName, String apiGroupVersion, Object item, Map<String, String> labels,
Map<String, String[]> labelsNot, Map<String, String[]> labelsIn, Map<String, String[]> labelsNotIn,
Map<String, String> fields, Map<String, String[]> fieldsNot, String resourceVersion,
Expand All @@ -94,6 +96,7 @@ public OperationContext(Client client, String plural, String namespace, String n
this.plural = plural;
setNamespace(namespace, defaultNamespace);
this.name = name;
this.subresource = subresource;
setApiGroupName(apiGroupName);
setApiGroupVersion(apiGroupVersion);
setLabels(labels);
Expand Down Expand Up @@ -191,6 +194,10 @@ public String getName() {
return name;
}

public String getSubresource() {
return subresource;
}

public String getApiGroupName() {
return apiGroupName;
}
Expand Down Expand Up @@ -349,6 +356,15 @@ public OperationContext withName(String name) {
return context;
}

public OperationContext withSubresource(String subresource) {
if (Objects.equals(this.subresource, subresource)) {
return this;
}
final OperationContext context = new OperationContext(this);
context.subresource = subresource;
return context;
}

public OperationContext withApiGroupName(String apiGroupName) {
if (Objects.equals(this.apiGroupName, apiGroupName)) {
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public class OperationSupport {
protected final String resourceT;
protected String namespace;
protected String name;
protected String subresource;
protected String apiGroupName;
protected String apiGroupVersion;
protected boolean dryRun;
Expand All @@ -95,6 +96,7 @@ public OperationSupport(OperationContext ctx) {
this.resourceT = ctx.getPlural();
this.namespace = ctx.getNamespace();
this.name = ctx.getName();
this.subresource = ctx.getSubresource();
this.apiGroupName = ctx.getApiGroupName();
this.dryRun = ctx.getDryRun();
if (Utils.isNotNullOrEmpty(ctx.getApiGroupVersion())) {
Expand Down Expand Up @@ -162,35 +164,44 @@ protected void addNamespacedUrlPathParts(List<String> parts, String namespace, S
parts.add("namespaces");
parts.add(namespace);
}
parts.add(type);

if (Utils.isNotNullOrEmpty(type)) {
parts.add(type);
}
}

public URL getNamespacedUrl() throws MalformedURLException {
return getNamespacedUrl(getNamespace());
}

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 {
public URL getResourceUrl(String namespace, String name, String... subresources) throws MalformedURLException {
String subresourcePath = URLUtils.pathJoin(subresources);
if (name == null) {
if (status) {
if (Utils.isNotNullOrEmpty(subresourcePath)) {
throw new KubernetesClientException("name not specified for an operation requiring one.");
}

return getNamespacedUrl(namespace);
}

String path = name;
if (Utils.isNotNullOrEmpty(subresourcePath)) {
path = URLUtils.pathJoin(path, subresourcePath);
}

return new URL(URLUtils.join(getNamespacedUrl(namespace).toString(), path));
}

public URL getResourceUrl(String namespace, String name, boolean status) throws MalformedURLException {
if (status) {
return new URL(URLUtils.join(getNamespacedUrl(namespace).toString(), name, "status"));
return getResourceUrl(namespace, name, "status");
}
return new URL(URLUtils.join(getNamespacedUrl(namespace).toString(), name));

return getResourceUrl(namespace, name, subresource);
}

public URL getResourceUrl() throws MalformedURLException {
if (name == null) {
return getNamespacedUrl();
}
return new URL(URLUtils.join(getNamespacedUrl().toString(), name));
return getResourceUrl(namespace, name, subresource);
}

public URL getResourceURLForWriteOperation(URL resourceURL) throws MalformedURLException {
Expand Down
Loading

0 comments on commit ecafb22

Please sign in to comment.