Skip to content

Commit f897e03

Browse files
authored
Add a KubectlApply processor for Spring.
1 parent b20e1c1 commit f897e03

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.spring.extended.manifests;
14+
15+
import io.kubernetes.client.common.KubernetesObject;
16+
import io.kubernetes.client.extended.kubectl.Kubectl;
17+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
18+
import io.kubernetes.client.openapi.ApiClient;
19+
import io.kubernetes.client.spring.extended.manifests.annotation.KubectlApply;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import org.springframework.beans.BeansException;
23+
import org.springframework.beans.factory.BeanCreationException;
24+
import org.springframework.beans.factory.BeanFactory;
25+
import org.springframework.beans.factory.BeanFactoryAware;
26+
import org.springframework.beans.factory.ListableBeanFactory;
27+
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.beans.factory.config.BeanPostProcessor;
29+
30+
public class KubernetesKubectlApplyProcessor implements BeanPostProcessor, BeanFactoryAware {
31+
32+
private static final Logger log = LoggerFactory.getLogger(KubernetesKubectlApplyProcessor.class);
33+
34+
private ListableBeanFactory beanFactory;
35+
36+
@Autowired private ApiClient apiClient;
37+
38+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
39+
if (!(bean instanceof KubernetesObject)) {
40+
return bean; // no-op
41+
}
42+
43+
KubectlApply apply = beanFactory.findAnnotationOnBean(beanName, KubectlApply.class);
44+
if (apply == null) {
45+
return bean;
46+
}
47+
48+
Class<? extends KubernetesObject> apiTypeClass =
49+
(Class<? extends KubernetesObject>) bean.getClass();
50+
51+
try {
52+
log.info("@KubectlApply ensuring resource upon bean {}", beanName);
53+
return apply(apiTypeClass, bean);
54+
} catch (KubectlException e) {
55+
log.error("Failed ensuring resource from @KubectlApply", e);
56+
throw new BeanCreationException("Failed ensuring resource from @KubectlApply", e);
57+
}
58+
}
59+
60+
public <ApiType extends KubernetesObject> ApiType apply(Class<ApiType> apiTypeClass, Object obj)
61+
throws KubectlException {
62+
return Kubectl.apply(apiTypeClass)
63+
.apiClient(this.apiClient)
64+
.resource((ApiType) obj)
65+
.execute(); // replaced the bean of the created status
66+
}
67+
68+
@Override
69+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
70+
this.beanFactory = (ListableBeanFactory) beanFactory;
71+
}
72+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.spring.extended.manifests.annotation;
14+
15+
import java.lang.annotation.ElementType;
16+
import java.lang.annotation.Retention;
17+
import java.lang.annotation.RetentionPolicy;
18+
import java.lang.annotation.Target;
19+
20+
@Target({ElementType.METHOD})
21+
@Retention(RetentionPolicy.RUNTIME)
22+
public @interface KubectlApply {
23+
boolean skipApiDiscovery() default false;
24+
}

spring/src/main/java/io/kubernetes/client/spring/extended/manifests/config/KubernetesManifestsAutoConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package io.kubernetes.client.spring.extended.manifests.config;
1414

1515
import io.kubernetes.client.spring.extended.manifests.KubernetesFromYamlProcessor;
16+
import io.kubernetes.client.spring.extended.manifests.KubernetesKubectlApplyProcessor;
1617
import io.kubernetes.client.spring.extended.manifests.KubernetesKubectlCreateProcessor;
1718
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
1819
import org.springframework.context.annotation.Bean;
@@ -33,4 +34,10 @@ public KubernetesKubectlCreateProcessor kubernetesKubectlCreateProcessor() {
3334
public KubernetesFromYamlProcessor kubernetesFromYamlProcessor() {
3435
return new KubernetesFromYamlProcessor();
3536
}
37+
38+
@Bean
39+
@ConditionalOnMissingBean
40+
public KubernetesKubectlApplyProcessor kubernetesKubectlApplyProcessor() {
41+
return new KubernetesKubectlApplyProcessor();
42+
}
3643
}

spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesManifestTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
1616
import static com.github.tomakehurst.wiremock.client.WireMock.get;
17+
import static com.github.tomakehurst.wiremock.client.WireMock.patch;
1718
import static com.github.tomakehurst.wiremock.client.WireMock.post;
1819
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
1920
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
@@ -26,7 +27,9 @@
2627
import io.kubernetes.client.openapi.Configuration;
2728
import io.kubernetes.client.openapi.models.V1Namespace;
2829
import io.kubernetes.client.openapi.models.V1ObjectMeta;
30+
import io.kubernetes.client.openapi.models.V1Pod;
2931
import io.kubernetes.client.openapi.models.V1ServiceAccount;
32+
import io.kubernetes.client.spring.extended.manifests.annotation.KubectlApply;
3033
import io.kubernetes.client.spring.extended.manifests.annotation.KubectlCreate;
3134
import io.kubernetes.client.util.ClientBuilder;
3235
import java.io.IOException;
@@ -84,6 +87,21 @@ public V1ServiceAccount testServiceAccount(
8487
.namespace(testNamespace.getMetadata().getName())
8588
.name("spring-boot-test-serviceaccount"));
8689
}
90+
91+
@Bean
92+
public KubernetesKubectlApplyProcessor kubernetesApplyManifestsProcessor() {
93+
return new KubernetesKubectlApplyProcessor();
94+
}
95+
96+
@Bean
97+
@KubectlApply
98+
public V1Pod testApplyPod(V1Namespace testNamespace) {
99+
return new V1Pod()
100+
.metadata(
101+
new V1ObjectMeta()
102+
.namespace(testNamespace.getMetadata().getName())
103+
.name("spring-boot-test-pod"));
104+
}
87105
}
88106

89107
@Autowired private ApiClient client;
@@ -92,6 +110,8 @@ public V1ServiceAccount testServiceAccount(
92110

93111
@Autowired private V1ServiceAccount createdServiceAccount;
94112

113+
@Autowired private V1Pod createdPod;
114+
95115
private static final V1Namespace returningCreatedNamespace =
96116
new V1Namespace()
97117
.metadata(
@@ -106,6 +126,14 @@ public V1ServiceAccount testServiceAccount(
106126
.name("spring-boot-test-serviceaccount")
107127
.putLabelsItem("created", "true"));
108128

129+
private static final V1Pod returningCreatedPod =
130+
new V1Pod()
131+
.metadata(
132+
new V1ObjectMeta()
133+
.name("spring-boot-test-pod")
134+
.namespace("spring-boot-test-namespace")
135+
.putLabelsItem("created", "true"));
136+
109137
static {
110138
wireMockRule.resetMappings();
111139
wireMockRule.resetScenarios();
@@ -129,6 +157,18 @@ public V1ServiceAccount testServiceAccount(
129157
.getJSON()
130158
.serialize(returningCreatedServiceAccount))));
131159

160+
wireMockRule.stubFor(
161+
patch(
162+
urlEqualTo(
163+
"/api/v1/namespaces/spring-boot-test-namespace/pods/spring-boot-test-pod?fieldManager=kubernetes-java-kubectl-apply&force=false"))
164+
.willReturn(
165+
aResponse()
166+
.withHeader("Content-Type", "application/json")
167+
.withBody(
168+
Configuration.getDefaultApiClient()
169+
.getJSON()
170+
.serialize(returningCreatedPod))));
171+
132172
try {
133173
wireMockRule.stubFor(
134174
get(urlPathEqualTo("/api"))
@@ -157,8 +197,10 @@ public V1ServiceAccount testServiceAccount(
157197
public void test() {
158198
assertNotNull(createdNamespace);
159199
assertNotNull(createdServiceAccount);
200+
assertNotNull(createdPod);
160201

161202
assertEquals("true", createdNamespace.getMetadata().getLabels().get("created"));
162203
assertEquals("true", createdServiceAccount.getMetadata().getLabels().get("created"));
204+
assertEquals("true", createdPod.getMetadata().getLabels().get("created"));
163205
}
164206
}

0 commit comments

Comments
 (0)