Skip to content

Commit 3cf16a9

Browse files
authored
Merge pull request #1405 from yue9944882/spring-boot-iac-2
spring-boot: manage bean as infrastructure via KubectlCreate annotation
2 parents 3ea30b0 + ad60063 commit 3cf16a9

File tree

10 files changed

+348
-1
lines changed

10 files changed

+348
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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.openapi.ApiException;
20+
import io.kubernetes.client.spring.extended.manifests.annotation.KubectlCreate;
21+
import java.net.HttpURLConnection;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
import org.springframework.beans.BeansException;
25+
import org.springframework.beans.factory.BeanCreationException;
26+
import org.springframework.beans.factory.BeanFactory;
27+
import org.springframework.beans.factory.BeanFactoryAware;
28+
import org.springframework.beans.factory.ListableBeanFactory;
29+
import org.springframework.beans.factory.annotation.Autowired;
30+
import org.springframework.beans.factory.config.BeanPostProcessor;
31+
32+
public class KubernetesKubectlCreateProcessor implements BeanPostProcessor, BeanFactoryAware {
33+
34+
private static final Logger log = LoggerFactory.getLogger(KubernetesKubectlCreateProcessor.class);
35+
36+
private ListableBeanFactory beanFactory;
37+
38+
@Autowired private ApiClient apiClient;
39+
40+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
41+
if (!(bean instanceof KubernetesObject)) {
42+
return bean; // no-op
43+
}
44+
45+
KubectlCreate create = beanFactory.findAnnotationOnBean(beanName, KubectlCreate.class);
46+
if (create == null) {
47+
return bean;
48+
}
49+
50+
Class<? extends KubernetesObject> apiTypeClass =
51+
(Class<? extends KubernetesObject>) bean.getClass();
52+
53+
try {
54+
log.info("@KubectlCreate ensuring resource upon bean {}", beanName);
55+
return create(apiTypeClass, bean);
56+
} catch (KubectlException e) {
57+
if (e.getCause() instanceof ApiException) {
58+
ApiException apiException = (ApiException) e.getCause();
59+
if (HttpURLConnection.HTTP_CONFLICT == apiException.getCode()) {
60+
// already exists
61+
log.info("Skipped processing {} @KubectlCreate resource already exists", beanName);
62+
return bean;
63+
}
64+
}
65+
log.error("Failed ensuring resource from @KubectlCreate", e);
66+
throw new BeanCreationException("Failed ensuring resource from @KubectlCreate", e);
67+
}
68+
}
69+
70+
public <ApiType extends KubernetesObject> ApiType create(Class<ApiType> apiTypeClass, Object obj)
71+
throws KubectlException {
72+
return Kubectl.create(apiTypeClass)
73+
.apiClient(this.apiClient)
74+
.resource((ApiType) obj)
75+
.execute(); // replaced the bean of the created status
76+
}
77+
78+
@Override
79+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
80+
this.beanFactory = (ListableBeanFactory) beanFactory;
81+
}
82+
}
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 KubectlCreate {
23+
boolean skipApiDiscovery() default false;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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.config;
14+
15+
import java.lang.annotation.*;
16+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
17+
18+
@Target(ElementType.TYPE)
19+
@Retention(RetentionPolicy.RUNTIME)
20+
@Documented
21+
@Inherited
22+
@ConditionalOnProperty(value = "kubernetes.manifests.enabled", matchIfMissing = true)
23+
public @interface ConditionalOnKubernetesManifestsEnabled {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.config;
14+
15+
import io.kubernetes.client.spring.extended.manifests.KubernetesKubectlCreateProcessor;
16+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
17+
import org.springframework.context.annotation.Bean;
18+
import org.springframework.context.annotation.Configuration;
19+
20+
@Configuration(proxyBeanMethods = false)
21+
@ConditionalOnKubernetesManifestsEnabled
22+
public class KubernetesManifestsAutoConfiguration {
23+
24+
@Bean
25+
@ConditionalOnMissingBean
26+
public KubernetesKubectlCreateProcessor kubernetesKubectlCreateProcessor() {
27+
return new KubernetesKubectlCreateProcessor();
28+
}
29+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
22
io.kubernetes.client.spring.extended.controller.config.KubernetesInformerAutoConfiguration, \
33
io.kubernetes.client.spring.extended.controller.config.KubernetesReconcilerAutoConfiguration, \
4-
io.kubernetes.client.spring.extended.network.config.KubernetesLoadBalancerAutoConfiguration
4+
io.kubernetes.client.spring.extended.network.config.KubernetesLoadBalancerAutoConfiguration, \
5+
io.kubernetes.client.spring.extended.manifests.config.KubernetesManifestsAutoConfiguration
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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 static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
16+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
17+
import static com.github.tomakehurst.wiremock.client.WireMock.post;
18+
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertNotNull;
22+
23+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
24+
import com.google.common.io.Resources;
25+
import io.kubernetes.client.openapi.ApiClient;
26+
import io.kubernetes.client.openapi.Configuration;
27+
import io.kubernetes.client.openapi.models.V1Namespace;
28+
import io.kubernetes.client.openapi.models.V1ObjectMeta;
29+
import io.kubernetes.client.openapi.models.V1ServiceAccount;
30+
import io.kubernetes.client.spring.extended.manifests.annotation.KubectlCreate;
31+
import io.kubernetes.client.util.ClientBuilder;
32+
import java.io.IOException;
33+
import java.nio.file.Files;
34+
import java.nio.file.Paths;
35+
import org.junit.ClassRule;
36+
import org.junit.Test;
37+
import org.junit.runner.RunWith;
38+
import org.springframework.beans.factory.annotation.Autowired;
39+
import org.springframework.boot.test.context.SpringBootTest;
40+
import org.springframework.boot.test.context.TestConfiguration;
41+
import org.springframework.context.annotation.Bean;
42+
import org.springframework.context.annotation.Import;
43+
import org.springframework.test.context.junit4.SpringRunner;
44+
45+
@RunWith(SpringRunner.class)
46+
@SpringBootTest
47+
@Import(KubernetesManifestTest.TestConfig.class)
48+
public class KubernetesManifestTest {
49+
50+
private static final String DISCOVERY_API = Resources.getResource("discovery-api.json").getPath();
51+
52+
private static final String DISCOVERY_APIV1 =
53+
Resources.getResource("discovery-api-v1.json").getPath();
54+
55+
private static final String DISCOVERY_APIS =
56+
Resources.getResource("discovery-apis.json").getPath();
57+
58+
@ClassRule public static WireMockRule wireMockRule = new WireMockRule(8288);
59+
60+
@TestConfiguration
61+
static class TestConfig {
62+
63+
@Bean
64+
public ApiClient testingApiClient() {
65+
ApiClient apiClient = new ClientBuilder().setBasePath(wireMockRule.baseUrl()).build();
66+
return apiClient;
67+
}
68+
69+
@Bean
70+
public KubernetesKubectlCreateProcessor kubernetesManifestsProcessor() {
71+
return new KubernetesKubectlCreateProcessor();
72+
}
73+
74+
@Bean
75+
@KubectlCreate
76+
public V1Namespace testNamespace() {
77+
return new V1Namespace().metadata(new V1ObjectMeta().name("spring-boot-test-namespace"));
78+
}
79+
80+
@Bean
81+
@KubectlCreate
82+
public V1ServiceAccount testServiceAccount(
83+
V1Namespace testNamespace) { // depends on testNamespace
84+
return new V1ServiceAccount()
85+
.metadata(
86+
new V1ObjectMeta()
87+
.namespace(testNamespace.getMetadata().getName())
88+
.name("spring-boot-test-serviceaccount"));
89+
}
90+
}
91+
92+
@Autowired private ApiClient client;
93+
94+
@Autowired private V1Namespace createdNamespace;
95+
96+
@Autowired private V1ServiceAccount createdServiceAccount;
97+
98+
private static final V1Namespace returningCreatedNamespace =
99+
new V1Namespace()
100+
.metadata(
101+
new V1ObjectMeta()
102+
.name("spring-boot-test-namespace")
103+
.putLabelsItem("created", "true"));
104+
105+
private static final V1ServiceAccount returningCreatedServiceAccount =
106+
new V1ServiceAccount()
107+
.metadata(
108+
new V1ObjectMeta()
109+
.name("spring-boot-test-serviceaccount")
110+
.putLabelsItem("created", "true"));
111+
112+
static {
113+
wireMockRule.resetMappings();
114+
wireMockRule.resetScenarios();
115+
wireMockRule.resetRequests();
116+
wireMockRule.stubFor(
117+
post(urlEqualTo("/api/v1/namespaces"))
118+
.willReturn(
119+
aResponse()
120+
.withHeader("Content-Type", "application/json")
121+
.withBody(
122+
Configuration.getDefaultApiClient()
123+
.getJSON()
124+
.serialize(returningCreatedNamespace))));
125+
wireMockRule.stubFor(
126+
post(urlEqualTo("/api/v1/namespaces/spring-boot-test-namespace/serviceaccounts"))
127+
.willReturn(
128+
aResponse()
129+
.withHeader("Content-Type", "application/json")
130+
.withBody(
131+
Configuration.getDefaultApiClient()
132+
.getJSON()
133+
.serialize(returningCreatedServiceAccount))));
134+
135+
try {
136+
wireMockRule.stubFor(
137+
get(urlPathEqualTo("/api"))
138+
.willReturn(
139+
aResponse()
140+
.withStatus(200)
141+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_API))))));
142+
wireMockRule.stubFor(
143+
get(urlPathEqualTo("/apis"))
144+
.willReturn(
145+
aResponse()
146+
.withStatus(200)
147+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIS))))));
148+
wireMockRule.stubFor(
149+
get(urlPathEqualTo("/api/v1"))
150+
.willReturn(
151+
aResponse()
152+
.withStatus(200)
153+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIV1))))));
154+
} catch (IOException e) {
155+
e.printStackTrace();
156+
}
157+
}
158+
159+
@Test
160+
public void test() {
161+
assertNotNull(createdNamespace);
162+
assertNotNull(createdServiceAccount);
163+
164+
assertEquals("true", createdNamespace.getMetadata().getLabels().get("created"));
165+
assertEquals("true", createdServiceAccount.getMetadata().getLabels().get("created"));
166+
}
167+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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 org.springframework.boot.SpringBootConfiguration;
16+
17+
@SpringBootConfiguration
18+
public class TestApplication {}

0 commit comments

Comments
 (0)