Skip to content

Commit 3a64c8b

Browse files
committed
wip
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent f9b15cb commit 3a64c8b

File tree

5 files changed

+233
-4
lines changed

5 files changed

+233
-4
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
apiVersion: v1
2+
kind: ServiceAccount
3+
metadata:
4+
name: operator
5+
6+
---
7+
apiVersion: v1
8+
kind: Pod
9+
metadata:
10+
name: operator
11+
spec:
12+
serviceAccountName: operator
13+
containers:
14+
- name: operator
15+
image: controller-namespace-deletion-operator
16+
imagePullPolicy: Never
17+
env:
18+
- name: POD_NAMESPACE
19+
valueFrom:
20+
fieldRef:
21+
fieldPath: metadata.namespace
22+
terminationGracePeriodSeconds: 30
23+
24+
---
25+
apiVersion: rbac.authorization.k8s.io/v1
26+
kind: RoleBinding
27+
metadata:
28+
name: operator
29+
finalizers:
30+
- controller.deletion/finalizer
31+
subjects:
32+
- kind: ServiceAccount
33+
name: operator
34+
roleRef:
35+
kind: Role
36+
name: operator
37+
apiGroup: rbac.authorization.k8s.io
38+
39+
---
40+
apiVersion: rbac.authorization.k8s.io/v1
41+
kind: Role
42+
metadata:
43+
name: operator
44+
finalizers:
45+
- controller.deletion/finalizer
46+
rules:
47+
- apiGroups:
48+
- "apiextensions.k8s.io"
49+
resources:
50+
- customresourcedefinitions
51+
verbs:
52+
- '*'
53+
- apiGroups:
54+
- "namespacedeletion.io"
55+
resources:
56+
- controllernamespacedeletioncustomresources
57+
- controllernamespacedeletioncustomresources/status
58+
verbs:
59+
- '*'
60+

sample-operators/controller-namespace-deletion/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
<image>gcr.io/distroless/java17-debian11</image>
7878
</from>
7979
<to>
80-
<image>leader-election-operator</image>
80+
<image>controller-namespace-deletion-operator</image>
8181
</to>
8282
</configuration>
8383
</plugin>
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package io.javaoperatorsdk.operator.sample;
22

3+
import io.fabric8.kubernetes.api.model.Namespaced;
34
import io.fabric8.kubernetes.client.CustomResource;
45
import io.fabric8.kubernetes.model.annotation.Group;
56
import io.fabric8.kubernetes.model.annotation.Version;
67

78
@Group("namespacedeletion.io")
89
@Version("v1")
910
public class ControllerNamespaceDeletionCustomResource
10-
extends CustomResource<ControllerNamespaceDeletionSpec, ControllerNamespaceDeletionStatus> {
11+
extends CustomResource<ControllerNamespaceDeletionSpec, ControllerNamespaceDeletionStatus>
12+
implements Namespaced {
1113

1214
}
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
11
package io.javaoperatorsdk.operator.sample;
22

3+
import java.time.LocalTime;
4+
35
import org.slf4j.Logger;
46
import org.slf4j.LoggerFactory;
57

8+
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
69
import io.javaoperatorsdk.operator.Operator;
10+
import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider;
11+
12+
import static java.time.temporal.ChronoUnit.SECONDS;
713

814
public class ControllerNamespaceDeletionOperator {
915

1016
private static final Logger log =
1117
LoggerFactory.getLogger(ControllerNamespaceDeletionOperator.class);
1218

1319
public static void main(String[] args) {
14-
Operator operator = new Operator();
1520

16-
operator.register(new ControllerNamespaceDeletionReconciler());
21+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
22+
log.info("Shutting down...");
23+
boolean allResourcesDeleted = waitUntilResourcesDeleted();
24+
log.info("All resources within timeout: {}", allResourcesDeleted);
25+
}));
26+
27+
Operator operator = new Operator();
28+
operator.register(new ControllerNamespaceDeletionReconciler(),
29+
ControllerConfigurationOverrider::watchingOnlyCurrentNamespace);
1730
operator.start();
1831
}
32+
33+
private static boolean waitUntilResourcesDeleted() {
34+
try (var client = new KubernetesClientBuilder().build()) {
35+
var startTime = LocalTime.now();
36+
while (startTime.until(LocalTime.now(), SECONDS) < 30) {
37+
var items =
38+
client.resources(ControllerNamespaceDeletionCustomResource.class).list().getItems();
39+
if (items.isEmpty()) {
40+
return true;
41+
}
42+
}
43+
return false;
44+
}
45+
}
1946
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package io.javaoperatorsdk.operator.sample;
2+
3+
import java.io.FileInputStream;
4+
import java.io.FileNotFoundException;
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.time.Duration;
8+
import java.util.List;
9+
import java.util.UUID;
10+
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Test;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
import io.fabric8.kubernetes.api.model.HasMetadata;
17+
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
18+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
19+
import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
20+
import io.fabric8.kubernetes.client.ConfigBuilder;
21+
import io.fabric8.kubernetes.client.KubernetesClient;
22+
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
23+
24+
import static io.javaoperatorsdk.operator.junit.AbstractOperatorExtension.CRD_READY_WAIT;
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.awaitility.Awaitility.await;
27+
28+
29+
class ControllerNamespaceDeletionE2E {
30+
31+
private static final Logger log = LoggerFactory.getLogger(ControllerNamespaceDeletionE2E.class);
32+
33+
public static final String TEST_RESOURCE_NAME = "test1";
34+
public static final String INITIAL_VALUE = "initial value";
35+
public static final String ROLE_ROLE_BINDING_FINALIZER = "controller.deletion/finalizer";
36+
public static final String RESOURCE_NAME = "operator";
37+
38+
String namespace;
39+
KubernetesClient client;
40+
41+
// not for local mode by design
42+
// @EnabledIfSystemProperty(named = "test.deployment", matches = "remote")
43+
@Test
44+
void customResourceCleanedUpOnNamespaceDeletion() {
45+
deployController();
46+
client.resource(testResource()).serverSideApply();
47+
48+
await().untilAsserted(() -> {
49+
var res = client.resources(ControllerNamespaceDeletionCustomResource.class)
50+
.inNamespace(namespace).withName(TEST_RESOURCE_NAME).get();
51+
assertThat(res.getStatus()).isNotNull();
52+
assertThat(res.getStatus().getValue()).isEqualTo(INITIAL_VALUE);
53+
});
54+
55+
client.namespaces().withName(namespace).delete();
56+
57+
await().timeout(Duration.ofSeconds(20)).untilAsserted(() -> {
58+
var ns = client.resources(ControllerNamespaceDeletionCustomResource.class)
59+
.inNamespace(namespace).withName(TEST_RESOURCE_NAME).get();
60+
assertThat(ns).isNull();
61+
});
62+
63+
log.info("Removing finalizers from role and role bing");
64+
removeRoleAndRoleBindingFinalizers();
65+
66+
await().untilAsserted(() -> {
67+
var ns = client.namespaces().withName(namespace).get();
68+
assertThat(ns).isNull();
69+
});
70+
}
71+
72+
private void removeRoleAndRoleBindingFinalizers() {
73+
var rolebinding =
74+
client.rbac().roleBindings().inNamespace(namespace).withName(RESOURCE_NAME).get();
75+
rolebinding.getFinalizers().clear();
76+
client.resource(rolebinding).update();
77+
78+
var role = client.rbac().roles().inNamespace(namespace).withName(RESOURCE_NAME).get();
79+
role.getFinalizers().clear();
80+
client.resource(role).update();
81+
82+
}
83+
84+
ControllerNamespaceDeletionCustomResource testResource() {
85+
var cr = new ControllerNamespaceDeletionCustomResource();
86+
cr.setMetadata(new ObjectMetaBuilder()
87+
.withName(TEST_RESOURCE_NAME)
88+
.withNamespace(namespace)
89+
.build());
90+
cr.setSpec(new ControllerNamespaceDeletionSpec());
91+
cr.getSpec().setValue(INITIAL_VALUE);
92+
return cr;
93+
}
94+
95+
96+
@BeforeEach
97+
void setup() {
98+
namespace = "controller-namespace-" + UUID.randomUUID();
99+
client = new KubernetesClientBuilder().withConfig(new ConfigBuilder()
100+
.withNamespace(namespace)
101+
.build()).build();
102+
applyCRD();
103+
client.namespaces().resource(new NamespaceBuilder().withNewMetadata().withName(namespace)
104+
.endMetadata().build()).create();
105+
}
106+
107+
void deployController() {
108+
try {
109+
List<HasMetadata> resources = client.load(new FileInputStream("k8s/operator.yaml")).items();
110+
resources.forEach(hm -> {
111+
hm.getMetadata().setNamespace(namespace);
112+
if (hm.getKind().equalsIgnoreCase("rolebinding")) {
113+
var crb = (RoleBinding) hm;
114+
for (var subject : crb.getSubjects()) {
115+
subject.setNamespace(namespace);
116+
}
117+
}
118+
});
119+
client.resourceList(resources)
120+
.inNamespace(namespace)
121+
.createOrReplace();
122+
123+
} catch (FileNotFoundException e) {
124+
throw new RuntimeException(e);
125+
}
126+
}
127+
128+
void applyCRD() {
129+
String path =
130+
"target/classes/META-INF/fabric8/controllernamespacedeletioncustomresources.namespacedeletion.io-v1.yml";
131+
try (InputStream is = new FileInputStream(path)) {
132+
final var crd = client.load(is);
133+
crd.serverSideApply();
134+
Thread.sleep(CRD_READY_WAIT);
135+
log.debug("Applied CRD with name: {}", crd.get().get(0).getMetadata().getName());
136+
} catch (InterruptedException | IOException e) {
137+
throw new RuntimeException(e);
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)