├── HELP.md ├── README.md ├── bin ├── foo.yaml ├── regen_crds.sh └── test.yaml ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── io │ │ └── spring │ │ ├── ControllersApplication.java │ │ └── models │ │ ├── V1Foo.java │ │ ├── V1FooList.java │ │ ├── V1FooSpec.java │ │ └── V1FooStatus.java └── resources │ ├── application.properties │ ├── configmap.yaml │ └── deployment.yaml └── test └── java └── io └── spring └── ControllersApplicationTests.java /HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | For further reference, please consider the following sections: 5 | 6 | * [Official Gradle documentation](https://docs.gradle.org) 7 | * [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.0.4/gradle-plugin/reference/html/) 8 | * [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.0.4/gradle-plugin/reference/html/#build-image) 9 | * [Spring for GraphQL](https://docs.spring.io/spring-boot/docs/3.0.4/reference/html/web.html#web.graphql) 10 | 11 | ### Guides 12 | The following guides illustrate how to use some features concretely: 13 | 14 | * [Building a GraphQL service](https://spring.io/guides/gs/graphql-server/) 15 | 16 | ### Additional Links 17 | These additional references should also help you: 18 | 19 | * [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # You're Going to Get Through This 2 | 3 | 4 | 5 | ## Generate a new spring native project from start.spring.io 6 | 7 | We'll need a new project from the [Spring Initializr](https://start.spring.io). Make sure to select `GraalVM Native Image` and `Lombok`. 8 | 9 | It'll generate everything and put it in the `io.spring.controllers` package. To keep things simpler, we've moved everything up one package, to `io.spring`. Delete the `controllers` package for both `src/main` and `src/test`. 10 | 11 | ## Customize the build file 12 | 13 | The Spring Initializr will get us most of the way (it does _a lot _ of code generation) but we need to add an extra dependency -- the official Java client for Kubernetes. 14 | 15 | If you're using Apache Maven, add this: 16 | 17 | ```xml 18 | 19 | io.kubernetes 20 | client-java-spring-aot-integration 21 | 17.0.0 22 | 23 | ``` 24 | 25 | If you're using Gradle, add this to your dependencies: 26 | 27 | ```groovy 28 | implementation 'io.kubernetes:client-java-spring-aot-integration:17.0.0' 29 | ``` 30 | 31 | ## Stage 32 | 33 | Copy `bin` and `k8s` from the source code to the new project generated from the Spring Initializr 34 | 35 | 36 | 37 | ## Deploy the CRD to Kubernetes 38 | 39 | ```shell 40 | k apply -f bin/foo.yaml 41 | ``` 42 | 43 | We should be able to do 44 | 45 | 46 | ```shell 47 | k get crds 48 | ``` 49 | 50 | And see the newly minted CRD. Now would also be an apropos time to show the audience the soul-annihilating-ly tedious definition of the CRD itself (`foo.yaml`). This CRD is why the Kubernetes community can't have nice things. 51 | 52 | We should also show the `test.yaml`, but don't apply it yet. This way people get the distinction between the archetypal definition of the CRD and an instance of the CRD. 53 | 54 | ## Run the Code Generator for the Image 55 | 56 | We'll need a little script to help us code-generate the Java code for our CRDs. Copy the `regen_crds.sh` script from our backup directory to the new project, in a directory called `bin`: 57 | 58 | ```shell 59 | ./bin/regen_crd.sh 60 | ``` 61 | 62 | ## And then a Miracle Happens 63 | 64 | There are several main concepts we need to understand before the code we're about to write makes sense. 65 | 66 | 67 | ### Controller 68 | 69 | Kubernetes is an edge-leveled reconciling controller. Basically, it will spin up a loop that evaluates some system state and if that system state should ever drift from the operator's desired state, the controller's job is to make that state so. So, llogically, a K8s CRD is two things: a CRD definition and a controller that reacts to the lifecycle of new instances of that CRD. We've already defined the CRD itself and looked at the generated code for the CRD instance itself. We're halfway there! We just need the controller itself. That'll be our first Spring `@Bean`. 70 | 71 | ```java 72 | 73 | @Bean 74 | Controller controller(SharedInformerFactory sharedInformerFactory, 75 | SharedIndexInformer fooNodeInformer, 76 | Reconciler reconciler) { 77 | var builder = ControllerBuilder // 78 | .defaultBuilder(sharedInformerFactory)// 79 | .watch((q) -> ControllerBuilder // 80 | .controllerWatchBuilder(V1Foo.class, q) 81 | .withResyncPeriod(Duration.ofHours(1)).build() // 82 | ) // 83 | .withWorkerCount(2); 84 | return builder 85 | .withReconciler(reconciler) // 86 | .withReadyFunc(fooNodeInformer::hasSynced) // optional: only start once the index is synced 87 | .withName("fooController") /// 88 | .build(); 89 | 90 | } 91 | ``` 92 | 93 | Things are broken! We don't have any of the three dependencies expressed here: `SharedInformerFactory`, `Reconciler`, and `SharedIndexInformer`. 94 | 95 | ```java 96 | 97 | @Bean 98 | SharedIndexInformer fooNodeInformer( 99 | SharedInformerFactory sharedInformerFactory, 100 | GenericKubernetesApi api) { 101 | return sharedInformerFactory.sharedIndexInformerFor(api, V1Foo.class, 0); 102 | } 103 | 104 | ``` 105 | 106 | This in turn implies a dependency on `GenericKubernetesApi`. 107 | 108 | ```java 109 | @Bean 110 | GenericKubernetesApi foosApi(ApiClient apiClient) { 111 | return new GenericKubernetesApi<>(V1Foo.class, V1FooList.class, "spring.io", "v1", 112 | "foos", apiClient); 113 | } 114 | ``` 115 | 116 | We'll also need a `GenericKubernetesApi` for `Deployment`s, too, so let's get that out of the way now: 117 | 118 | 119 | ```java 120 | @Bean 121 | GenericKubernetesApi deploymentsApi(ApiClient apiClient) { 122 | return new GenericKubernetesApi<>(V1Deployment.class, V1DeploymentList.class, 123 | "", "v1", "deployments", 124 | apiClient); 125 | } 126 | 127 | ``` 128 | 129 | They're identical, except for their generic parameters and the string definitions of the group, and pluralized form of their nouns. These API clients let us talk to the API server about a given type of CRD, in this case `Foo` and `Deployment`, respectively. 130 | 131 | We need the `SharedIndexInformer`, too. 132 | 133 | What's an `Informer`, you ask? An informer "is a" - and we're not making this iup - controller together with the ability to distribute its `Queue`-related operations to an appropriate event handler. There are `SharedInformer`s that share data across multiple instances of the `Informer` so that they're not duplicated. A `SharedInformer` has a shared data cache and is capable of distributing notifications for changes to the cache to multiple listeners who registered with it. There is one behavior change compared to a standard `Informer`: when you receive a notification, the cache will be _at least_ as fresh as the notification, but it _may_ be more fresh. You should not depend on the contents of the cache exactly matching the state implied by the notification. The notification is binding. `SharedIndexInformer` only adds one more thing to the picture: the ability to lookup items by various keys. So, a controller sometimes needs a conceptually-a-controller to be a controller. Got it? Got it. 134 | 135 | Next, we'll need to define the `Reconciler` itself: 136 | 137 | ```java 138 | @Bean 139 | Reconciler reconciler( 140 | @Value("classpath:/deployment.yaml") Resource resourceForDeploymentYaml, 141 | AppsV1Api coreV1Api, 142 | SharedIndexInformer fooNodeInformer, 143 | GenericKubernetesApi deploymentApi) { 144 | return new FooReconciler(coreV1Api, resourceForDeploymentYaml, fooNodeInformer, deploymentApi); 145 | } 146 | ``` 147 | 148 | Here's where the rubber meets the road: our reconciler will create a new `Deployment` every time a new `Foo` is created. We like you too much to programmatically build up the `Deployment` from scratch in Java, so we'll just reuse a pre-written YAML definition (`/deployment.yaml`) of a `Deployment` and then reify it, changing some of its parameters, and submit that. 149 | 150 | We'll also need references fo the `GenericKubernetesAPi` for `Deployments` and a new thing, called the `AppsV1Api`. This API sidesteps all the caching and indexing and allows us to talk directly to the API server. You could achieve this without using the API, but it simplifies things sometimes and it's instructional to see it in action, so: 151 | 152 | ```java 153 | @Bean 154 | AppsV1Api appsV1Api(ApiClient apiClient) { 155 | return new AppsV1Api(apiClient); 156 | } 157 | ``` 158 | 159 | ## Deploy an Instance of the `foo` Object 160 | 161 | ```shell 162 | k apply -f bin/test.yaml 163 | ``` 164 | 165 | ## Run the Program 166 | 167 | If you're using Apache Maven: `./mvnw spring-boot:run` 168 | 169 | If you're using Gradle: `./gradlew bootRun` 170 | 171 | 172 | ## Compile a GraalVM Native Image 173 | 174 | 175 | If you're using Apache Maven: `./mvnw -Pnative native:compile` 176 | 177 | If you're using Gradle: `./gradlew nativeCompile` 178 | 179 | 180 | ## Resources 181 | - [we found the following post supremely useful for navigating this nightmare world](https://lairdnelson.wordpress.com/2018/01/07/understanding-kubernetes-tools-cache-package-part-3/) 182 | - [Generating models from CRD YAML definitions for fun and profit](https://github.com/kubernetes-client/java/blob/master/docs/generate-model-from-third-party-resources.md) 183 | 184 | -------------------------------------------------------------------------------- /bin/foo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: foos.spring.io 5 | spec: 6 | preserveUnknownFields: false 7 | group: spring.io 8 | names: 9 | kind: Foo 10 | listKind: FooList 11 | plural: foos 12 | singular: foo 13 | scope: Namespaced 14 | versions: 15 | - name: v1 16 | served: true 17 | storage: true 18 | # additionalPrinterColumns: 19 | # - jsonPath: .spec.name 20 | # description: name 21 | # name: Foo 22 | # type: string 23 | subresources: 24 | status: {} 25 | schema: 26 | openAPIV3Schema: 27 | description: Foo is the Schema for the foo API 28 | properties: 29 | apiVersion: 30 | description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 31 | type: string 32 | kind: 33 | description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 34 | type: string 35 | metadata: 36 | type: object 37 | spec: 38 | description: FooSpec defines the desired state of Foo 39 | properties: 40 | name: 41 | description: Name your foo, fool! 42 | type: string 43 | type: object 44 | status: 45 | description: FooStatus defines the observed state of Foo 46 | properties: 47 | name: 48 | type: string 49 | type: object 50 | type: object 51 | status: 52 | acceptedNames: 53 | kind: "" 54 | plural: "" 55 | conditions: [] 56 | storedVersions: [] -------------------------------------------------------------------------------- /bin/regen_crds.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # the value u pass to -n and -p (io.spring) HAS to match the value 4 | # in the crd definition itself, but reversed. So, in the foo.yaml we 5 | # have `group: spring.io`, and this Java package becomes io.spring 6 | CURRENT_DIR=$(cd `dirname $0` && pwd) 7 | LOCAL_MANIFEST_FILE=${CURRENT_DIR}/foo.yaml 8 | echo "CURRENT_DIR=${CURRENT_DIR}" 9 | echo "LOCAL_MANIFEST_FILE=${LOCAL_MANIFEST_FILE}" 10 | mkdir -p /tmp/java && cd /tmp/java 11 | docker run \ 12 | --rm \ 13 | -v "$LOCAL_MANIFEST_FILE":"$LOCAL_MANIFEST_FILE" \ 14 | -v /var/run/docker.sock:/var/run/docker.sock \ 15 | -v "$(pwd)":"$(pwd)" \ 16 | -ti \ 17 | --network host \ 18 | ghcr.io/kubernetes-client/java/crd-model-gen:v1.0.6 \ 19 | /generate.sh \ 20 | -u $LOCAL_MANIFEST_FILE \ 21 | -n io.spring \ 22 | -p io.spring \ 23 | -o "$(pwd)" 24 | cp -r /tmp/java/src/main/java/io/spring/models ${CURRENT_DIR}/../src/main/java/io/spring/ 25 | -------------------------------------------------------------------------------- /bin/test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: spring.io/v1 2 | kind: Foo 3 | metadata: 4 | name: demo2 5 | spec: 6 | name: SpringOne Tour 1 -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.0.4' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | id 'org.graalvm.buildtools.native' version '0.9.20' 6 | } 7 | 8 | group = 'io.spring' 9 | version = '0.0.1-SNAPSHOT' 10 | sourceCompatibility = '17' 11 | 12 | configurations { 13 | compileOnly { 14 | extendsFrom annotationProcessor 15 | } 16 | } 17 | 18 | repositories { 19 | mavenCentral() 20 | } 21 | 22 | dependencies { 23 | compileOnly 'org.projectlombok:lombok' 24 | annotationProcessor 'org.projectlombok:lombok' 25 | implementation 'io.kubernetes:client-java-spring-aot-integration:17.0.0' 26 | implementation 'org.springframework.boot:spring-boot-starter' 27 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 28 | } 29 | 30 | tasks.named('test') { 31 | useJUnitPlatform() 32 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubernetes-native-java/controllers-101/237953b529104eae847902d82b96eb29a4132579/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'controllers' 2 | -------------------------------------------------------------------------------- /src/main/java/io/spring/ControllersApplication.java: -------------------------------------------------------------------------------- 1 | package io.spring; 2 | 3 | import io.spring.models.V1Foo; 4 | import io.spring.models.V1FooList; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import io.kubernetes.client.common.KubernetesObject; 8 | import io.kubernetes.client.extended.controller.Controller; 9 | import io.kubernetes.client.extended.controller.builder.ControllerBuilder; 10 | import io.kubernetes.client.extended.controller.builder.DefaultControllerBuilder; 11 | import io.kubernetes.client.extended.controller.reconciler.Reconciler; 12 | import io.kubernetes.client.extended.controller.reconciler.Result; 13 | import io.kubernetes.client.informer.SharedIndexInformer; 14 | import io.kubernetes.client.informer.SharedInformerFactory; 15 | import io.kubernetes.client.openapi.ApiClient; 16 | import io.kubernetes.client.openapi.ApiException; 17 | import io.kubernetes.client.openapi.apis.AppsV1Api; 18 | import io.kubernetes.client.openapi.apis.CoreV1Api; 19 | import io.kubernetes.client.openapi.models.*; 20 | import io.kubernetes.client.util.Yaml; 21 | import io.kubernetes.client.util.generic.GenericKubernetesApi; 22 | import lombok.SneakyThrows; 23 | import lombok.extern.slf4j.Slf4j; 24 | import org.springframework.aot.hint.RuntimeHints; 25 | import org.springframework.aot.hint.RuntimeHintsRegistrar; 26 | import org.springframework.beans.factory.annotation.Value; 27 | import org.springframework.boot.ApplicationRunner; 28 | import org.springframework.context.annotation.Bean; 29 | import org.springframework.context.annotation.ImportRuntimeHints; 30 | import org.springframework.core.io.ClassPathResource; 31 | import org.springframework.core.io.Resource; 32 | import org.springframework.util.Assert; 33 | import org.springframework.util.FileCopyUtils; 34 | 35 | import java.io.InputStreamReader; 36 | import java.time.Duration; 37 | import java.time.Instant; 38 | import java.util.List; 39 | import java.util.Map; 40 | import java.util.Objects; 41 | import java.util.concurrent.Executors; 42 | 43 | 44 | @Slf4j 45 | @ImportRuntimeHints(ControllersApplication.FooControllerRuntimeHints.class) 46 | @SpringBootApplication 47 | public class ControllersApplication { 48 | 49 | public static void main(String[] args) { 50 | SpringApplication.run(ControllersApplication.class, args); 51 | } 52 | 53 | static class FooControllerRuntimeHints implements RuntimeHintsRegistrar { 54 | 55 | @Override 56 | public void registerHints(RuntimeHints hints, ClassLoader classLoader) { 57 | for (var path : new String[] { "/configmap.yaml", "/deployment.yaml" }) { 58 | hints.resources().registerResource(new ClassPathResource(path)); 59 | } 60 | } 61 | 62 | } 63 | 64 | @Bean 65 | GenericKubernetesApi foosApi(ApiClient apiClient) { 66 | return new GenericKubernetesApi<>(V1Foo.class, V1FooList.class, "spring.io", "v1", "foos", apiClient); 67 | } 68 | 69 | @Bean 70 | SharedIndexInformer foosSharedIndexInformer(SharedInformerFactory sharedInformerFactory, 71 | GenericKubernetesApi api) { 72 | return sharedInformerFactory.sharedIndexInformerFor(api, V1Foo.class, 0); 73 | } 74 | 75 | @Bean 76 | AppsV1Api appsV1Api(ApiClient apiClient) { 77 | return new AppsV1Api(apiClient); 78 | } 79 | 80 | @Bean 81 | CoreV1Api coreV1Api(ApiClient apiClient) { 82 | return new CoreV1Api(apiClient); 83 | } 84 | 85 | @Bean(destroyMethod = "shutdown") 86 | Controller fooController(SharedInformerFactory sharedInformerFactory, SharedIndexInformer fooNodeInformer, 87 | Reconciler reconciler) { 88 | 89 | DefaultControllerBuilder builder = ControllerBuilder // 90 | .defaultBuilder(sharedInformerFactory)// 91 | .watch(fooQ -> ControllerBuilder // 92 | .controllerWatchBuilder(V1Foo.class, fooQ)// 93 | .withResyncPeriod(Duration.ofSeconds(1))// 94 | .build()) // 95 | .withWorkerCount(2); 96 | return builder// 97 | .withReconciler(reconciler) // 98 | .withReadyFunc(fooNodeInformer::hasSynced) // optional: only start once 99 | // the index is synced 100 | .withName("fooController") /// 101 | .build(); 102 | 103 | } 104 | 105 | @Bean 106 | ApplicationRunner runner(SharedInformerFactory sharedInformerFactory, Controller controller) { 107 | var executorService = Executors.newCachedThreadPool(); 108 | return args -> executorService.execute(() -> { 109 | sharedInformerFactory.startAllRegisteredInformers(); 110 | controller.run(); 111 | }); 112 | } 113 | 114 | @FunctionalInterface 115 | interface ApiSupplier { 116 | 117 | T get() throws ApiException; 118 | 119 | } 120 | 121 | /** 122 | * the Reconciler won't get an event telling it that the cluster has changed, but 123 | * instead it looks at cluster state and determines that something has changed 124 | */ 125 | @Bean 126 | Reconciler reconciler(@Value("classpath:configmap.yaml") Resource configMapYaml, 127 | @Value("classpath:deployment.yaml") Resource deploymentYaml, 128 | SharedIndexInformer v1FooSharedIndexInformer, AppsV1Api appsV1Api, CoreV1Api coreV1Api) { 129 | return request -> { 130 | try { 131 | // create new one on k apply -f foo.yaml 132 | String requestName = request.getName(); 133 | String key = request.getNamespace() + '/' + requestName; 134 | V1Foo foo = v1FooSharedIndexInformer.getIndexer().getByKey(key); 135 | if (foo == null) { // deleted. we use ownerreferences so dont need to do 136 | // anything special here 137 | return new Result(false); 138 | } 139 | 140 | String namespace = foo.getMetadata().getNamespace(); 141 | String pretty = "true"; 142 | String dryRun = null; 143 | String fieldManager = ""; 144 | String fieldValidation = ""; 145 | 146 | // parameterize configmap 147 | String configMapName = "configmap-" + requestName; 148 | V1ConfigMap configMap = loadYamlAs(configMapYaml, V1ConfigMap.class); 149 | String html = "

Hello, " + foo.getSpec().getName() + "

"; 150 | configMap.getData().put("index.html", html); 151 | configMap.getMetadata().setName(configMapName); 152 | createOrUpdate(V1ConfigMap.class, () -> { 153 | addOwnerReference(requestName, foo, configMap); 154 | return coreV1Api.createNamespacedConfigMap(namespace, configMap, pretty, dryRun, fieldManager, 155 | fieldValidation); 156 | }, () -> coreV1Api.replaceNamespacedConfigMap(configMapName, namespace, configMap, 157 | pretty, dryRun, fieldManager, fieldValidation)); 158 | 159 | // parameterize deployment 160 | String deploymentName = "deployment-" + requestName; 161 | V1Deployment deployment = loadYamlAs(deploymentYaml, V1Deployment.class); 162 | deployment.getMetadata().setName(deploymentName); 163 | List volumes = deployment.getSpec().getTemplate().getSpec().getVolumes(); 164 | Assert.isTrue(volumes.size() == 1, () -> "there should be only one V1Volume"); 165 | volumes.forEach(vol -> vol.getConfigMap().setName(configMapName)); 166 | createOrUpdate(V1Deployment.class, () -> { 167 | deployment.getSpec().getTemplate().getMetadata() 168 | .setAnnotations(Map.of("bootiful-update", Instant.now().toString())); 169 | addOwnerReference(requestName, foo, deployment); 170 | return appsV1Api.createNamespacedDeployment(namespace, deployment, pretty, dryRun, fieldManager, 171 | fieldValidation); 172 | }, () -> { 173 | updateAnnotation(deployment); 174 | return appsV1Api.replaceNamespacedDeployment(deploymentName, namespace, deployment, pretty, dryRun, 175 | fieldManager, fieldValidation); 176 | }); 177 | } // 178 | catch (Throwable e) { 179 | log.error("we've got an outer error.", e); 180 | return new Result(true, Duration.ofSeconds(60)); 181 | } 182 | return new Result(false); 183 | }; 184 | } 185 | 186 | private void updateAnnotation(V1Deployment deployment) { 187 | Objects.requireNonNull(Objects.requireNonNull(deployment.getSpec()).getTemplate().getMetadata()) 188 | .setAnnotations(Map.of("bootiful-update", Instant.now().toString())); 189 | } 190 | 191 | static private void createOrUpdate(Class clazz, ApiSupplier creator, ApiSupplier updater) { 192 | try { 193 | creator.get(); 194 | log.info("It worked! we created a new " + clazz.getName() + "!"); 195 | } // 196 | catch (ApiException throwable) { 197 | int code = throwable.getCode(); 198 | if (code == 409) { // already exists 199 | log.info("the " + clazz.getName() + " already exists. Replacing."); 200 | try { 201 | updater.get(); 202 | log.info("successfully updated the " + clazz.getName()); 203 | } 204 | catch (ApiException ex) { 205 | log.error("got an error on update", ex); 206 | } 207 | } // 208 | else { 209 | log.info("got an exception with code " + code + " while trying to create the " + clazz.getName()); 210 | } 211 | } 212 | } 213 | 214 | private static V1ObjectMeta addOwnerReference(String requestName, V1Foo foo, KubernetesObject kubernetesObject) { 215 | Assert.notNull(foo, () -> "the V1Foo must not be null"); 216 | return kubernetesObject.getMetadata().addOwnerReferencesItem(new V1OwnerReference().kind(foo.getKind()) 217 | .apiVersion(foo.getApiVersion()).controller(true).uid(foo.getMetadata().getUid()).name(requestName)); 218 | } 219 | 220 | @SneakyThrows 221 | private static T loadYamlAs(Resource resource, Class clzz) { 222 | var yaml = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); 223 | return Yaml.loadAs(yaml, clzz); 224 | } 225 | 226 | } -------------------------------------------------------------------------------- /src/main/java/io/spring/models/V1Foo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kubernetes 3 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) 4 | * 5 | * The version of the OpenAPI document: v1.21.1 6 | * 7 | * 8 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 9 | * https://openapi-generator.tech 10 | * Do not edit the class manually. 11 | */ 12 | 13 | 14 | package io.spring.models; 15 | 16 | import java.util.Objects; 17 | import java.util.Arrays; 18 | import com.google.gson.TypeAdapter; 19 | import com.google.gson.annotations.JsonAdapter; 20 | import com.google.gson.annotations.SerializedName; 21 | import com.google.gson.stream.JsonReader; 22 | import com.google.gson.stream.JsonWriter; 23 | import io.kubernetes.client.openapi.models.V1ObjectMeta; 24 | import io.spring.models.V1FooSpec; 25 | import io.spring.models.V1FooStatus; 26 | import io.swagger.annotations.ApiModel; 27 | import io.swagger.annotations.ApiModelProperty; 28 | import java.io.IOException; 29 | 30 | /** 31 | * Foo is the Schema for the foo API 32 | */ 33 | @ApiModel(description = "Foo is the Schema for the foo API") 34 | @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", date = "2023-03-14T03:40:36.644Z[Etc/UTC]") 35 | public class V1Foo implements io.kubernetes.client.common.KubernetesObject { 36 | public static final String SERIALIZED_NAME_API_VERSION = "apiVersion"; 37 | @SerializedName(SERIALIZED_NAME_API_VERSION) 38 | private String apiVersion; 39 | 40 | public static final String SERIALIZED_NAME_KIND = "kind"; 41 | @SerializedName(SERIALIZED_NAME_KIND) 42 | private String kind; 43 | 44 | public static final String SERIALIZED_NAME_METADATA = "metadata"; 45 | @SerializedName(SERIALIZED_NAME_METADATA) 46 | private V1ObjectMeta metadata = null; 47 | 48 | public static final String SERIALIZED_NAME_SPEC = "spec"; 49 | @SerializedName(SERIALIZED_NAME_SPEC) 50 | private V1FooSpec spec; 51 | 52 | public static final String SERIALIZED_NAME_STATUS = "status"; 53 | @SerializedName(SERIALIZED_NAME_STATUS) 54 | private V1FooStatus status; 55 | 56 | 57 | public V1Foo apiVersion(String apiVersion) { 58 | 59 | this.apiVersion = apiVersion; 60 | return this; 61 | } 62 | 63 | /** 64 | * APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 65 | * @return apiVersion 66 | **/ 67 | @javax.annotation.Nullable 68 | @ApiModelProperty(value = "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources") 69 | 70 | public String getApiVersion() { 71 | return apiVersion; 72 | } 73 | 74 | 75 | public void setApiVersion(String apiVersion) { 76 | this.apiVersion = apiVersion; 77 | } 78 | 79 | 80 | public V1Foo kind(String kind) { 81 | 82 | this.kind = kind; 83 | return this; 84 | } 85 | 86 | /** 87 | * Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 88 | * @return kind 89 | **/ 90 | @javax.annotation.Nullable 91 | @ApiModelProperty(value = "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds") 92 | 93 | public String getKind() { 94 | return kind; 95 | } 96 | 97 | 98 | public void setKind(String kind) { 99 | this.kind = kind; 100 | } 101 | 102 | 103 | public V1Foo metadata(V1ObjectMeta metadata) { 104 | 105 | this.metadata = metadata; 106 | return this; 107 | } 108 | 109 | /** 110 | * Get metadata 111 | * @return metadata 112 | **/ 113 | @javax.annotation.Nullable 114 | @ApiModelProperty(value = "") 115 | 116 | public V1ObjectMeta getMetadata() { 117 | return metadata; 118 | } 119 | 120 | 121 | public void setMetadata(V1ObjectMeta metadata) { 122 | this.metadata = metadata; 123 | } 124 | 125 | 126 | public V1Foo spec(V1FooSpec spec) { 127 | 128 | this.spec = spec; 129 | return this; 130 | } 131 | 132 | /** 133 | * Get spec 134 | * @return spec 135 | **/ 136 | @javax.annotation.Nullable 137 | @ApiModelProperty(value = "") 138 | 139 | public V1FooSpec getSpec() { 140 | return spec; 141 | } 142 | 143 | 144 | public void setSpec(V1FooSpec spec) { 145 | this.spec = spec; 146 | } 147 | 148 | 149 | public V1Foo status(V1FooStatus status) { 150 | 151 | this.status = status; 152 | return this; 153 | } 154 | 155 | /** 156 | * Get status 157 | * @return status 158 | **/ 159 | @javax.annotation.Nullable 160 | @ApiModelProperty(value = "") 161 | 162 | public V1FooStatus getStatus() { 163 | return status; 164 | } 165 | 166 | 167 | public void setStatus(V1FooStatus status) { 168 | this.status = status; 169 | } 170 | 171 | 172 | @Override 173 | public boolean equals(Object o) { 174 | if (this == o) { 175 | return true; 176 | } 177 | if (o == null || getClass() != o.getClass()) { 178 | return false; 179 | } 180 | V1Foo v1Foo = (V1Foo) o; 181 | return Objects.equals(this.apiVersion, v1Foo.apiVersion) && 182 | Objects.equals(this.kind, v1Foo.kind) && 183 | Objects.equals(this.metadata, v1Foo.metadata) && 184 | Objects.equals(this.spec, v1Foo.spec) && 185 | Objects.equals(this.status, v1Foo.status); 186 | } 187 | 188 | @Override 189 | public int hashCode() { 190 | return Objects.hash(apiVersion, kind, metadata, spec, status); 191 | } 192 | 193 | 194 | @Override 195 | public String toString() { 196 | StringBuilder sb = new StringBuilder(); 197 | sb.append("class V1Foo {\n"); 198 | sb.append(" apiVersion: ").append(toIndentedString(apiVersion)).append("\n"); 199 | sb.append(" kind: ").append(toIndentedString(kind)).append("\n"); 200 | sb.append(" metadata: ").append(toIndentedString(metadata)).append("\n"); 201 | sb.append(" spec: ").append(toIndentedString(spec)).append("\n"); 202 | sb.append(" status: ").append(toIndentedString(status)).append("\n"); 203 | sb.append("}"); 204 | return sb.toString(); 205 | } 206 | 207 | /** 208 | * Convert the given object to string with each line indented by 4 spaces 209 | * (except the first line). 210 | */ 211 | private String toIndentedString(Object o) { 212 | if (o == null) { 213 | return "null"; 214 | } 215 | return o.toString().replace("\n", "\n "); 216 | } 217 | 218 | } 219 | 220 | -------------------------------------------------------------------------------- /src/main/java/io/spring/models/V1FooList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kubernetes 3 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) 4 | * 5 | * The version of the OpenAPI document: v1.21.1 6 | * 7 | * 8 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 9 | * https://openapi-generator.tech 10 | * Do not edit the class manually. 11 | */ 12 | 13 | 14 | package io.spring.models; 15 | 16 | import java.util.Objects; 17 | import java.util.Arrays; 18 | import com.google.gson.TypeAdapter; 19 | import com.google.gson.annotations.JsonAdapter; 20 | import com.google.gson.annotations.SerializedName; 21 | import com.google.gson.stream.JsonReader; 22 | import com.google.gson.stream.JsonWriter; 23 | import io.kubernetes.client.openapi.models.V1ListMeta; 24 | import io.spring.models.V1Foo; 25 | import io.swagger.annotations.ApiModel; 26 | import io.swagger.annotations.ApiModelProperty; 27 | import java.io.IOException; 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | 31 | /** 32 | * FooList is a list of Foo 33 | */ 34 | @ApiModel(description = "FooList is a list of Foo") 35 | @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", date = "2023-03-14T03:40:36.644Z[Etc/UTC]") 36 | public class V1FooList implements io.kubernetes.client.common.KubernetesListObject { 37 | public static final String SERIALIZED_NAME_API_VERSION = "apiVersion"; 38 | @SerializedName(SERIALIZED_NAME_API_VERSION) 39 | private String apiVersion; 40 | 41 | public static final String SERIALIZED_NAME_ITEMS = "items"; 42 | @SerializedName(SERIALIZED_NAME_ITEMS) 43 | private List items = new ArrayList<>(); 44 | 45 | public static final String SERIALIZED_NAME_KIND = "kind"; 46 | @SerializedName(SERIALIZED_NAME_KIND) 47 | private String kind; 48 | 49 | public static final String SERIALIZED_NAME_METADATA = "metadata"; 50 | @SerializedName(SERIALIZED_NAME_METADATA) 51 | private V1ListMeta metadata = null; 52 | 53 | 54 | public V1FooList apiVersion(String apiVersion) { 55 | 56 | this.apiVersion = apiVersion; 57 | return this; 58 | } 59 | 60 | /** 61 | * APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 62 | * @return apiVersion 63 | **/ 64 | @javax.annotation.Nullable 65 | @ApiModelProperty(value = "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources") 66 | 67 | public String getApiVersion() { 68 | return apiVersion; 69 | } 70 | 71 | 72 | public void setApiVersion(String apiVersion) { 73 | this.apiVersion = apiVersion; 74 | } 75 | 76 | 77 | public V1FooList items(List items) { 78 | 79 | this.items = items; 80 | return this; 81 | } 82 | 83 | public V1FooList addItemsItem(V1Foo itemsItem) { 84 | this.items.add(itemsItem); 85 | return this; 86 | } 87 | 88 | /** 89 | * List of foos. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md 90 | * @return items 91 | **/ 92 | @ApiModelProperty(required = true, value = "List of foos. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md") 93 | 94 | public List getItems() { 95 | return items; 96 | } 97 | 98 | 99 | public void setItems(List items) { 100 | this.items = items; 101 | } 102 | 103 | 104 | public V1FooList kind(String kind) { 105 | 106 | this.kind = kind; 107 | return this; 108 | } 109 | 110 | /** 111 | * Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 112 | * @return kind 113 | **/ 114 | @javax.annotation.Nullable 115 | @ApiModelProperty(value = "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds") 116 | 117 | public String getKind() { 118 | return kind; 119 | } 120 | 121 | 122 | public void setKind(String kind) { 123 | this.kind = kind; 124 | } 125 | 126 | 127 | public V1FooList metadata(V1ListMeta metadata) { 128 | 129 | this.metadata = metadata; 130 | return this; 131 | } 132 | 133 | /** 134 | * Get metadata 135 | * @return metadata 136 | **/ 137 | @javax.annotation.Nullable 138 | @ApiModelProperty(value = "") 139 | 140 | public V1ListMeta getMetadata() { 141 | return metadata; 142 | } 143 | 144 | 145 | public void setMetadata(V1ListMeta metadata) { 146 | this.metadata = metadata; 147 | } 148 | 149 | 150 | @Override 151 | public boolean equals(Object o) { 152 | if (this == o) { 153 | return true; 154 | } 155 | if (o == null || getClass() != o.getClass()) { 156 | return false; 157 | } 158 | V1FooList v1FooList = (V1FooList) o; 159 | return Objects.equals(this.apiVersion, v1FooList.apiVersion) && 160 | Objects.equals(this.items, v1FooList.items) && 161 | Objects.equals(this.kind, v1FooList.kind) && 162 | Objects.equals(this.metadata, v1FooList.metadata); 163 | } 164 | 165 | @Override 166 | public int hashCode() { 167 | return Objects.hash(apiVersion, items, kind, metadata); 168 | } 169 | 170 | 171 | @Override 172 | public String toString() { 173 | StringBuilder sb = new StringBuilder(); 174 | sb.append("class V1FooList {\n"); 175 | sb.append(" apiVersion: ").append(toIndentedString(apiVersion)).append("\n"); 176 | sb.append(" items: ").append(toIndentedString(items)).append("\n"); 177 | sb.append(" kind: ").append(toIndentedString(kind)).append("\n"); 178 | sb.append(" metadata: ").append(toIndentedString(metadata)).append("\n"); 179 | sb.append("}"); 180 | return sb.toString(); 181 | } 182 | 183 | /** 184 | * Convert the given object to string with each line indented by 4 spaces 185 | * (except the first line). 186 | */ 187 | private String toIndentedString(Object o) { 188 | if (o == null) { 189 | return "null"; 190 | } 191 | return o.toString().replace("\n", "\n "); 192 | } 193 | 194 | } 195 | 196 | -------------------------------------------------------------------------------- /src/main/java/io/spring/models/V1FooSpec.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kubernetes 3 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) 4 | * 5 | * The version of the OpenAPI document: v1.21.1 6 | * 7 | * 8 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 9 | * https://openapi-generator.tech 10 | * Do not edit the class manually. 11 | */ 12 | 13 | 14 | package io.spring.models; 15 | 16 | import java.util.Objects; 17 | import java.util.Arrays; 18 | import com.google.gson.TypeAdapter; 19 | import com.google.gson.annotations.JsonAdapter; 20 | import com.google.gson.annotations.SerializedName; 21 | import com.google.gson.stream.JsonReader; 22 | import com.google.gson.stream.JsonWriter; 23 | import io.swagger.annotations.ApiModel; 24 | import io.swagger.annotations.ApiModelProperty; 25 | import java.io.IOException; 26 | 27 | /** 28 | * FooSpec defines the desired state of Foo 29 | */ 30 | @ApiModel(description = "FooSpec defines the desired state of Foo") 31 | @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", date = "2023-03-14T03:40:36.644Z[Etc/UTC]") 32 | public class V1FooSpec { 33 | public static final String SERIALIZED_NAME_NAME = "name"; 34 | @SerializedName(SERIALIZED_NAME_NAME) 35 | private String name; 36 | 37 | 38 | public V1FooSpec name(String name) { 39 | 40 | this.name = name; 41 | return this; 42 | } 43 | 44 | /** 45 | * Name your foo, fool! 46 | * @return name 47 | **/ 48 | @javax.annotation.Nullable 49 | @ApiModelProperty(value = "Name your foo, fool!") 50 | 51 | public String getName() { 52 | return name; 53 | } 54 | 55 | 56 | public void setName(String name) { 57 | this.name = name; 58 | } 59 | 60 | 61 | @Override 62 | public boolean equals(Object o) { 63 | if (this == o) { 64 | return true; 65 | } 66 | if (o == null || getClass() != o.getClass()) { 67 | return false; 68 | } 69 | V1FooSpec v1FooSpec = (V1FooSpec) o; 70 | return Objects.equals(this.name, v1FooSpec.name); 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | return Objects.hash(name); 76 | } 77 | 78 | 79 | @Override 80 | public String toString() { 81 | StringBuilder sb = new StringBuilder(); 82 | sb.append("class V1FooSpec {\n"); 83 | sb.append(" name: ").append(toIndentedString(name)).append("\n"); 84 | sb.append("}"); 85 | return sb.toString(); 86 | } 87 | 88 | /** 89 | * Convert the given object to string with each line indented by 4 spaces 90 | * (except the first line). 91 | */ 92 | private String toIndentedString(Object o) { 93 | if (o == null) { 94 | return "null"; 95 | } 96 | return o.toString().replace("\n", "\n "); 97 | } 98 | 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/main/java/io/spring/models/V1FooStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kubernetes 3 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) 4 | * 5 | * The version of the OpenAPI document: v1.21.1 6 | * 7 | * 8 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 9 | * https://openapi-generator.tech 10 | * Do not edit the class manually. 11 | */ 12 | 13 | 14 | package io.spring.models; 15 | 16 | import java.util.Objects; 17 | import java.util.Arrays; 18 | import com.google.gson.TypeAdapter; 19 | import com.google.gson.annotations.JsonAdapter; 20 | import com.google.gson.annotations.SerializedName; 21 | import com.google.gson.stream.JsonReader; 22 | import com.google.gson.stream.JsonWriter; 23 | import io.swagger.annotations.ApiModel; 24 | import io.swagger.annotations.ApiModelProperty; 25 | import java.io.IOException; 26 | 27 | /** 28 | * FooStatus defines the observed state of Foo 29 | */ 30 | @ApiModel(description = "FooStatus defines the observed state of Foo") 31 | @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", date = "2023-03-14T03:40:36.644Z[Etc/UTC]") 32 | public class V1FooStatus { 33 | public static final String SERIALIZED_NAME_NAME = "name"; 34 | @SerializedName(SERIALIZED_NAME_NAME) 35 | private String name; 36 | 37 | 38 | public V1FooStatus name(String name) { 39 | 40 | this.name = name; 41 | return this; 42 | } 43 | 44 | /** 45 | * Get name 46 | * @return name 47 | **/ 48 | @javax.annotation.Nullable 49 | @ApiModelProperty(value = "") 50 | 51 | public String getName() { 52 | return name; 53 | } 54 | 55 | 56 | public void setName(String name) { 57 | this.name = name; 58 | } 59 | 60 | 61 | @Override 62 | public boolean equals(Object o) { 63 | if (this == o) { 64 | return true; 65 | } 66 | if (o == null || getClass() != o.getClass()) { 67 | return false; 68 | } 69 | V1FooStatus v1FooStatus = (V1FooStatus) o; 70 | return Objects.equals(this.name, v1FooStatus.name); 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | return Objects.hash(name); 76 | } 77 | 78 | 79 | @Override 80 | public String toString() { 81 | StringBuilder sb = new StringBuilder(); 82 | sb.append("class V1FooStatus {\n"); 83 | sb.append(" name: ").append(toIndentedString(name)).append("\n"); 84 | sb.append("}"); 85 | return sb.toString(); 86 | } 87 | 88 | /** 89 | * Convert the given object to string with each line indented by 4 spaces 90 | * (except the first line). 91 | */ 92 | private String toIndentedString(Object o) { 93 | if (o == null) { 94 | return "null"; 95 | } 96 | return o.toString().replace("\n", "\n "); 97 | } 98 | 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/main/resources/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: index-html-configmap 5 | namespace: default 6 | data: 7 | index.html: | 8 |

Hello, world!

-------------------------------------------------------------------------------- /src/main/resources/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | namespace: default 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: nginx 10 | replicas: 2 11 | template: 12 | metadata: 13 | labels: 14 | app: nginx 15 | 16 | spec: 17 | 18 | containers: 19 | - name: nginx 20 | image: nginx:latest 21 | ports: 22 | - containerPort: 80 23 | volumeMounts: 24 | - name: nginx-index-file 25 | mountPath: /usr/share/nginx/html/ 26 | volumes: 27 | - name: nginx-index-file 28 | configMap: 29 | name: index-html-configmap 30 | -------------------------------------------------------------------------------- /src/test/java/io/spring/ControllersApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.spring; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ControllersApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------