diff --git a/.github/actions/graalvm-build/action.yml b/.github/actions/graalvm-build/action.yml index 7ff2362b5..846032cfe 100644 --- a/.github/actions/graalvm-build/action.yml +++ b/.github/actions/graalvm-build/action.yml @@ -1,10 +1,6 @@ #TODO move this action to https://github.com/micronaut-projects/github-actions name: Build steps for GitHub gradle workflow description: Perform build steps for gradle workflow -inputs: - java: - description: java version - required: true runs: using: "composite" steps: diff --git a/.github/actions/graalvm-pre-build/action.yml b/.github/actions/graalvm-pre-build/action.yml index d907d78f9..0cddbd0ec 100644 --- a/.github/actions/graalvm-pre-build/action.yml +++ b/.github/actions/graalvm-pre-build/action.yml @@ -27,4 +27,4 @@ runs: - name: Optional setup step shell: bash run: | - [ -f ./setup.sh ] && ./setup.sh || true + [ -f ./setup.sh ] && ./setup.sh || [ ! -f ./setup.sh ] diff --git a/.github/actions/gradle-build/action.yml b/.github/actions/gradle-build/action.yml index 8ef6cdfcd..c1236be29 100644 --- a/.github/actions/gradle-build/action.yml +++ b/.github/actions/gradle-build/action.yml @@ -6,6 +6,9 @@ inputs: description: provide --parallel to gradle build required: false default: 'true' + java: + description: java version + required: true runs: using: "composite" steps: diff --git a/.github/actions/gradle-pre-build/action.yml b/.github/actions/gradle-pre-build/action.yml index 7d0ed60c9..9344ff5cf 100644 --- a/.github/actions/gradle-pre-build/action.yml +++ b/.github/actions/gradle-pre-build/action.yml @@ -23,4 +23,4 @@ runs: - name: Optional setup step shell: bash run: | - [ -f ./setup.sh ] && ./setup.sh || true + [ -f ./setup.sh ] && ./setup.sh || [ ! -f ./setup.sh ] diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml index 3be7e1c61..02ffb75be 100644 --- a/.github/workflows/graalvm.yml +++ b/.github/workflows/graalvm.yml @@ -17,39 +17,63 @@ jobs: build: if: github.repository != 'micronaut-projects/micronaut-project-template' runs-on: ubuntu-latest + env: + OCI_CLI_USER: ${{ secrets.OCI_USER }} + OCI_CLI_TENANCY: ${{ secrets.OCI_TENANCY }} + OCI_CLI_FINGERPRINT: ${{ secrets.OCI_FINGERPRINT }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_KEY_FILE }} + OCI_CLI_REGION: ${{ secrets.OCI_REGION }} strategy: matrix: java: [ '17' ] graalvm: ['latest'] - k8s: [ '1.21' ] steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Configure Kubectl + uses: oracle-actions/configure-kubectl-oke@v1.3.1 + id: test-configure-kubectl-oke-action + with: + cluster: ${{ secrets.OKE_CLUSTER_OCID }} + - name: Run Kubectl + run: kubectl get nodes -A - name: Pre-build - uses: ./.github/actions/gradle-pre-build + uses: ./.github/actions/graalvm-pre-build id: pre-build env: - K8S_VERSION: ${{ matrix.k8s }} GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + GIT_COMMIT_HASH: ${{ github.sha }} + OCI_CLI_REGION: ${{ secrets.OCI_REGION }} + JOB_ID: graalvm-java-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} + OCI_TENANCY_NAME: ${{ secrets.OCI_TENANCY_NAME }} + JAVA_VERSION: ${{ matrix.java }} + AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + OCIR_USERNAME: ${{ secrets.OCIR_USERNAME }} with: java: ${{ matrix.java }} graalvm: ${{ matrix.graalvm }} - name: Build uses: ./.github/actions/graalvm-build id: build - with: - java: ${{ matrix.java }} env: GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} GH_USERNAME: ${{ secrets.GH_USERNAME }} GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + IMAGE_TAG: 'java-${{ matrix.java }}-${{ github.sha }}' + IMAGE_PREFIX: '${{ secrets.OCI_REGION }}.ocir.io/${{ secrets.OCI_TENANCY_NAME }}/' + KUBERNETES_TRUST_CERTIFICATES: 'true' + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Post-build uses: ./.github/actions/graalvm-post-build + if: always() id: post-build env: - K8S_VERSION: ${{ matrix.k8s }} + JOB_ID: graalvm-java-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} with: java: ${{ matrix.java }} diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index a86e2e2f3..119aebcb0 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -17,36 +17,61 @@ jobs: build: if: github.repository != 'micronaut-projects/micronaut-project-template' runs-on: ubuntu-latest + env: + OCI_CLI_USER: ${{ secrets.OCI_USER }} + OCI_CLI_TENANCY: ${{ secrets.OCI_TENANCY }} + OCI_CLI_FINGERPRINT: ${{ secrets.OCI_FINGERPRINT }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_KEY_FILE }} + OCI_CLI_REGION: ${{ secrets.OCI_REGION }} strategy: matrix: java: ['17'] k8s: ['1.21'] steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Configure Kubectl + uses: oracle-actions/configure-kubectl-oke@v1.3.1 + id: test-configure-kubectl-oke-action + with: + cluster: ${{ secrets.OKE_CLUSTER_OCID }} + - name: Run Kubectl + run: kubectl get nodes -A - name: Pre-build uses: ./.github/actions/gradle-pre-build id: pre-build env: - K8S_VERSION: ${{ matrix.k8s }} GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + OCI_CLI_REGION: ${{ secrets.OCI_REGION }} + OCI_TENANCY_NAME: ${{ secrets.OCI_TENANCY_NAME }} + AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + OCIR_USERNAME: ${{ secrets.OCIR_USERNAME }} + GIT_COMMIT_HASH: ${{ github.sha }} + JOB_ID: gardle-${{ matrix.java }}-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} + JAVA_VERSION: ${{ matrix.java }} with: java: ${{ matrix.java }} - name: Build uses: ./.github/actions/gradle-build id: build - with: - java: ${{ matrix.java }} env: GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} - GH_USERNAME: ${{ secrets.GH_USERNAME }} GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + IMAGE_TAG: 'java-${{ matrix.java }}-${{ github.sha }}' + IMAGE_PREFIX: '${{ secrets.OCI_REGION }}.ocir.io/${{ secrets.OCI_TENANCY_NAME }}/' + KUBERNETES_TRUST_CERTIFICATES: 'true' + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} - name: Post-build uses: ./.github/actions/gradle-post-build id: post-build + if: always() env: GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} GH_USERNAME: ${{ secrets.GH_USERNAME }} @@ -56,7 +81,7 @@ jobs: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} GH_TOKEN: ${{ secrets.GH_TOKEN }} - K8S_VERSION: ${{ matrix.k8s }} + JOB_ID: gardle-${{ matrix.java }}-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} with: java: ${{ matrix.java }} - name: Publish-Github-Pages diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 152f41a3e..547a3cd2b 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -16,6 +16,12 @@ on: jobs: build: if: github.repository != 'micronaut-projects/micronaut-project-template' + env: + OCI_CLI_USER: ${{ secrets.OCI_USER }} + OCI_CLI_TENANCY: ${{ secrets.OCI_TENANCY }} + OCI_CLI_FINGERPRINT: ${{ secrets.OCI_FINGERPRINT }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_KEY_FILE }} + OCI_CLI_REGION: ${{ secrets.OCI_REGION }} runs-on: ubuntu-latest steps: # https://github.com/actions/virtual-environments/issues/709 @@ -39,13 +45,27 @@ jobs: with: distribution: 'temurin' java-version: 17 + - name: Configure Kubectl + uses: oracle-actions/configure-kubectl-oke@v1.3.1 + id: test-configure-kubectl-oke-action + with: + cluster: ${{ secrets.OKE_CLUSTER_OCID }} + - name: Run Kubectl + run: kubectl get nodes -A - name: Optional setup step env: GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + GIT_COMMIT_HASH: ${{ github.sha }} + JOB_ID: sonar-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} + OCI_CLI_REGION: ${{ secrets.OCI_REGION }} + OCI_TENANCY_NAME: ${{ secrets.OCI_TENANCY_NAME }} + JAVA_VERSION: '11' + AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + OCIR_USERNAME: ${{ secrets.OCIR_USERNAME }} run: | - [ -f ./setup.sh ] && ./setup.sh || true + [ -f ./setup.sh ] && ./setup.sh || [ ! -f ./setup.sh ] - name: Analyse with Gradle run: | ./gradlew check sonarqube --no-daemon --continue --no-build-cache @@ -54,5 +74,17 @@ jobs: GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + IMAGE_TAG: 'java-11-${{ github.sha }}' + IMAGE_PREFIX: '${{ secrets.OCI_REGION }}.ocir.io/${{ secrets.OCI_TENANCY_NAME }}/' SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + KUBERNETES_TRUST_CERTIFICATES: 'true' + - name: Optional teardown step + if: always() + env: + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + JOB_ID: sonar-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} + run: | + [ -f ./teardown.sh ] && ./teardown.sh || true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 18fbec355..dfcb67c45 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,13 +37,13 @@ kind create cluster --wait 5m There is a script that will take care to create the example services docker images used in tests and load them into the cluster: ```shell script -./setup-kubernetes.sh +./setup-kubernetes-kind.sh ``` Note: In case you want to use other than default Kind cluster name `kind`, run the setup script with cluster name as firdt parameter: ```shell script -./setup-kubernetes.sh +./setup-kubernetes-kind.sh ``` ### Docker Desktop @@ -138,6 +138,8 @@ If you want to setup other kind resources just override the `setupFixture` metho } ``` +## CI/CD +For running test on CI/CD, micronaut-kubernetes module is using [Oracle Kubernetes Engine](https://www.oracle.com/cloud/cloud-native/container-engine-kubernetes/). With [vcluster](https://www.vcluster.com/) we are creating virtual cluster for every test run inside Oracle Kubernetes Engine. Docker images that produced during build process are specially tagged by hash of commit and java version. Then images are pushed to [Oracle Container Registry](https://www.oracle.com/cloud/cloud-native/container-registry/). Every test run will create virtual cluster and pick docker image based on the tag described above. ## Checkstyle diff --git a/examples/micronaut-client/src/test/groovy/micronaut/client/HelloControllerSpec.groovy b/examples/micronaut-client/src/test/groovy/micronaut/client/HelloControllerSpec.groovy index 4afacc650..01ecefea4 100644 --- a/examples/micronaut-client/src/test/groovy/micronaut/client/HelloControllerSpec.groovy +++ b/examples/micronaut-client/src/test/groovy/micronaut/client/HelloControllerSpec.groovy @@ -21,6 +21,8 @@ import io.micronaut.context.annotation.Requires as MicronautRequires @Property(name = "spec.name", value = "HelloControllerSpec") @Property(name = "spec.reuseNamespace", value = "false") @Property(name = "kubernetes.client.namespace", value = "micronaut-example-client") +@Property(name = "micronaut.http.client.connect-timeout", value = "100s") +@Property(name = "micronaut.http.client.read-timeout", value = "100s") @Requires({ TestUtils.kubernetesApiAvailable() }) class HelloControllerSpec extends KubernetesSpecification { diff --git a/examples/micronaut-client/src/test/groovy/micronaut/client/KubernetesHealthIndicatorSpec.groovy b/examples/micronaut-client/src/test/groovy/micronaut/client/KubernetesHealthIndicatorSpec.groovy index c080b5269..e1684cf2a 100644 --- a/examples/micronaut-client/src/test/groovy/micronaut/client/KubernetesHealthIndicatorSpec.groovy +++ b/examples/micronaut-client/src/test/groovy/micronaut/client/KubernetesHealthIndicatorSpec.groovy @@ -1,5 +1,6 @@ package micronaut.client +import groovy.util.logging.Slf4j import io.micronaut.context.annotation.Property import io.micronaut.context.env.Environment import io.micronaut.http.annotation.Get @@ -14,8 +15,10 @@ import jakarta.inject.Inject import io.micronaut.context.annotation.Requires as MicronautRequires + @MicronautTest(environments = Environment.KUBERNETES) @Property(name = "spec.name", value = "KubernetesHealthIndicatorSpec") +@Property(name = "kubernetes.client.namespace", value = "kubernetes-health-indicator") @Property(name = "spec.reuseNamespace", value = "false") @Requires({ TestUtils.kubernetesApiAvailable() }) class KubernetesHealthIndicatorSpec extends KubernetesSpecification { @@ -24,6 +27,9 @@ class KubernetesHealthIndicatorSpec extends KubernetesSpecification { @Shared ServiceClient client + @Property(name = "image.tag") + Optional imageTag + def setupSpec() { operations.portForwardService("example-service", namespace, 8081, 9999) } @@ -31,6 +37,7 @@ class KubernetesHealthIndicatorSpec extends KubernetesSpecification { void "it works"() { when: Map details = client.health().details + String tagName = imageTag.orElse("latest") then: details.kubernetes.name == "micronaut-service" @@ -41,7 +48,7 @@ class KubernetesHealthIndicatorSpec extends KubernetesSpecification { details.kubernetes.details.podIP details.kubernetes.details.hostIP details.kubernetes.details.containerStatuses.first().name == "example-service" - details.kubernetes.details.containerStatuses.first().image.endsWith "example-service:latest" + details.kubernetes.details.containerStatuses.first().image.endsWith "example-service:" + tagName details.kubernetes.details.containerStatuses.first().ready == true } diff --git a/examples/micronaut-client/src/test/groovy/micronaut/client/utils/KubernetesSpecification.groovy b/examples/micronaut-client/src/test/groovy/micronaut/client/utils/KubernetesSpecification.groovy index 25c92bcf1..13699c51f 100644 --- a/examples/micronaut-client/src/test/groovy/micronaut/client/utils/KubernetesSpecification.groovy +++ b/examples/micronaut-client/src/test/groovy/micronaut/client/utils/KubernetesSpecification.groovy @@ -20,15 +20,18 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.KubernetesOperations -import jakarta.inject.Inject import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import jakarta.inject.Inject + /** * Abstract specification encapsulating setup of test namespace. * @@ -53,6 +56,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +105,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +217,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/examples/micronaut-kubernetes-informer/src/test/groovy/micronaut/informer/SecretInformerControllerSpec.groovy b/examples/micronaut-kubernetes-informer/src/test/groovy/micronaut/informer/SecretInformerControllerSpec.groovy index a7caea5a3..e958b724f 100644 --- a/examples/micronaut-kubernetes-informer/src/test/groovy/micronaut/informer/SecretInformerControllerSpec.groovy +++ b/examples/micronaut-kubernetes-informer/src/test/groovy/micronaut/informer/SecretInformerControllerSpec.groovy @@ -1,5 +1,6 @@ package micronaut.informer +import groovy.util.logging.Slf4j import io.fabric8.kubernetes.api.model.ContainerBuilder import io.fabric8.kubernetes.api.model.ContainerPortBuilder import io.fabric8.kubernetes.api.model.HTTPGetActionBuilder @@ -32,9 +33,9 @@ import java.util.concurrent.TimeUnit @MicronautTest(environments = [Environment.KUBERNETES], startApplication = false) @Property(name = "spec.name", value = "SecretInformerControllerSpec") -@Property(name = "spec.reuseNamespace", value = "false") @Property(name = "kubernetes.client.namespace", value = "micronaut-example-informer") @Requires({ TestUtils.kubernetesApiAvailable() }) +@Slf4j class SecretInformerControllerSpec extends KubernetesSpecification { @Inject @@ -45,6 +46,9 @@ class SecretInformerControllerSpec extends KubernetesSpecification { def setupFixture(String namespace) { createNamespaceSafe(namespace) createBaseResources(namespace) + def imageName = getImageName("micronaut-kubernetes-informer-example") + log.info("Image name: ${imageName}") + def client = operations.getClient(namespace) def informerDeployment = client.apps().deployments().createOrReplace( new DeploymentBuilder() @@ -63,8 +67,8 @@ class SecretInformerControllerSpec extends KubernetesSpecification { .withSpec(new PodSpecBuilder() .withContainers(new ContainerBuilder() .withName("informer") - .withImage("micronaut-kubernetes-informer-example") - .withImagePullPolicy("Never") + .withImage(imageName) + .withImagePullPolicy("IfNotPresent") .withPorts(new ContainerPortBuilder() .withName("http") .withContainerPort(8080) @@ -112,10 +116,16 @@ class SecretInformerControllerSpec extends KubernetesSpecification { } void "test all"() { + PollingConditions conditions = new PollingConditions(timeout: 30, delay: 2) expect: - testClient.all().size() == 4 - testClient.secret("test-secret") - testClient.secret("test-secret").data.containsKey("username") + conditions.eventually { + // kind adds "default-token" secret to every namespace. That's why we check with >= to make sure it works both with OKE and kind + testClient.all().size() >= 3 + testClient.secret("mounted-secret") + testClient.secret("another-secret") + testClient.secret("test-secret") + testClient.secret("test-secret").data.containsKey("username") + } } @@ -134,7 +144,10 @@ class SecretInformerControllerSpec extends KubernetesSpecification { then: conditions.eventually { - testClient.all().size() == 5 + testClient.all().size() >= 4 + testClient.secret("mounted-secret") + testClient.secret("another-secret") + testClient.secret("test-secret") testClient.secret(secretName) } @@ -143,7 +156,11 @@ class SecretInformerControllerSpec extends KubernetesSpecification { then: conditions.eventually { - testClient.all().size() == 4 + testClient.all().size() >= 3 + testClient.secret("mounted-secret") + testClient.secret("another-secret") + testClient.secret("test-secret") + !testClient.secret(secretName) } } diff --git a/examples/micronaut-kubernetes-informer/src/test/groovy/micronaut/informer/utils/KubernetesSpecification.groovy b/examples/micronaut-kubernetes-informer/src/test/groovy/micronaut/informer/utils/KubernetesSpecification.groovy index de330691b..a6d94cc62 100644 --- a/examples/micronaut-kubernetes-informer/src/test/groovy/micronaut/informer/utils/KubernetesSpecification.groovy +++ b/examples/micronaut-kubernetes-informer/src/test/groovy/micronaut/informer/utils/KubernetesSpecification.groovy @@ -20,15 +20,18 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.KubernetesOperations -import jakarta.inject.Inject import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import jakarta.inject.Inject + /** * Abstract specification encapsulating setup of test namespace. * @@ -53,6 +56,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +105,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +217,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/examples/micronaut-kubernetes-operator/src/test/groovy/micronaut/operator/ConfigMapOperatorControllerSpec.groovy b/examples/micronaut-kubernetes-operator/src/test/groovy/micronaut/operator/ConfigMapOperatorControllerSpec.groovy index d75e9f279..f2b489c38 100644 --- a/examples/micronaut-kubernetes-operator/src/test/groovy/micronaut/operator/ConfigMapOperatorControllerSpec.groovy +++ b/examples/micronaut-kubernetes-operator/src/test/groovy/micronaut/operator/ConfigMapOperatorControllerSpec.groovy @@ -1,11 +1,13 @@ package micronaut.operator +import groovy.util.logging.Slf4j import io.fabric8.kubernetes.api.model.* import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder import io.micronaut.context.annotation.Property import io.micronaut.context.env.Environment import micronaut.operator.utils.KubernetesSpecification +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.TestUtils import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Requires @@ -18,6 +20,7 @@ import java.util.concurrent.TimeUnit @Property(name = "spec.reuseNamespace", value = "false") @Property(name = "kubernetes.client.namespace", value = "micronaut-example-operator") @Requires({ TestUtils.kubernetesApiAvailable() }) +@Slf4j class ConfigMapOperatorControllerSpec extends KubernetesSpecification { static String configMapName = "new-configmap" @@ -25,6 +28,9 @@ class ConfigMapOperatorControllerSpec extends KubernetesSpecification { @Override def setupFixture(String namespace) { createNamespaceSafe(namespace) + def imageName = getImageName("micronaut-kubernetes-operator-example") + log.info("Image name: ${imageName}") + operations.deleteConfigMap(configMapName, namespace) operations.createRole("operator-reconciler-role", namespace, @@ -58,8 +64,8 @@ class ConfigMapOperatorControllerSpec extends KubernetesSpecification { .withSpec(new PodSpecBuilder() .withContainers(new ContainerBuilder() .withName("operator") - .withImage("micronaut-kubernetes-operator-example") - .withImagePullPolicy("Never") + .withImage(imageName) + .withImagePullPolicy("IfNotPresent") .withPorts(new ContainerPortBuilder() .withName("http") .withContainerPort(8080) diff --git a/examples/micronaut-kubernetes-operator/src/test/groovy/micronaut/operator/utils/KubernetesSpecification.groovy b/examples/micronaut-kubernetes-operator/src/test/groovy/micronaut/operator/utils/KubernetesSpecification.groovy index 8f7a40933..fa7964f3f 100644 --- a/examples/micronaut-kubernetes-operator/src/test/groovy/micronaut/operator/utils/KubernetesSpecification.groovy +++ b/examples/micronaut-kubernetes-operator/src/test/groovy/micronaut/operator/utils/KubernetesSpecification.groovy @@ -20,15 +20,18 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.KubernetesOperations -import jakarta.inject.Inject import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import jakarta.inject.Inject + /** * Abstract specification encapsulating setup of test namespace. * @@ -53,6 +56,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +105,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +217,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/kubernetes-client-reactor/src/test/groovy/io/micronaut/kubernetes/client/reactor/utils/KubernetesSpecification.groovy b/kubernetes-client-reactor/src/test/groovy/io/micronaut/kubernetes/client/reactor/utils/KubernetesSpecification.groovy index df1c0950d..55db4bea9 100644 --- a/kubernetes-client-reactor/src/test/groovy/io/micronaut/kubernetes/client/reactor/utils/KubernetesSpecification.groovy +++ b/kubernetes-client-reactor/src/test/groovy/io/micronaut/kubernetes/client/reactor/utils/KubernetesSpecification.groovy @@ -20,15 +20,18 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.KubernetesOperations -import jakarta.inject.Inject import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import jakarta.inject.Inject + /** * Abstract specification encapsulating setup of test namespace. * @@ -53,6 +56,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +105,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +217,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/kubernetes-client-rxjava2/src/test/groovy/io/micronaut/kubernetes/client/rxjava2/utils/KubernetesSpecification.groovy b/kubernetes-client-rxjava2/src/test/groovy/io/micronaut/kubernetes/client/rxjava2/utils/KubernetesSpecification.groovy index c9d3cbbbe..04b51705f 100644 --- a/kubernetes-client-rxjava2/src/test/groovy/io/micronaut/kubernetes/client/rxjava2/utils/KubernetesSpecification.groovy +++ b/kubernetes-client-rxjava2/src/test/groovy/io/micronaut/kubernetes/client/rxjava2/utils/KubernetesSpecification.groovy @@ -20,15 +20,18 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.KubernetesOperations -import jakarta.inject.Inject import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import jakarta.inject.Inject + /** * Abstract specification encapsulating setup of test namespace. * @@ -53,6 +56,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +105,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +217,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/kubernetes-client-rxjava3/src/test/groovy/io/micronaut/kubernetes/client/rxjava3/utils/KubernetesSpecification.groovy b/kubernetes-client-rxjava3/src/test/groovy/io/micronaut/kubernetes/client/rxjava3/utils/KubernetesSpecification.groovy index 9a2a29ed6..a7f2e311f 100644 --- a/kubernetes-client-rxjava3/src/test/groovy/io/micronaut/kubernetes/client/rxjava3/utils/KubernetesSpecification.groovy +++ b/kubernetes-client-rxjava3/src/test/groovy/io/micronaut/kubernetes/client/rxjava3/utils/KubernetesSpecification.groovy @@ -20,15 +20,18 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.KubernetesOperations -import jakarta.inject.Inject import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import jakarta.inject.Inject + /** * Abstract specification encapsulating setup of test namespace. * @@ -53,6 +56,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +105,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +217,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/kubernetes-client/src/main/java/io/micronaut/kubernetes/client/ApiClientFactory.java b/kubernetes-client/src/main/java/io/micronaut/kubernetes/client/ApiClientFactory.java index b94b15375..e8899dc56 100644 --- a/kubernetes-client/src/main/java/io/micronaut/kubernetes/client/ApiClientFactory.java +++ b/kubernetes-client/src/main/java/io/micronaut/kubernetes/client/ApiClientFactory.java @@ -35,6 +35,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; +import java.time.Duration; import java.util.concurrent.ExecutorService; import static io.micronaut.scheduling.TaskExecutors.IO; @@ -84,6 +85,9 @@ public ClientBuilder clientBuilder(ApiClientConfiguration apiClientConfiguration clientBuilder = ClientBuilder.standard(); } updateBuilderConfiguration(apiClientConfiguration, clientBuilder); + if (clientBuilder.getReadTimeout().isZero()) { + clientBuilder.setReadTimeout(Duration.ofSeconds(10)); + } return clientBuilder; } diff --git a/kubernetes-client/src/main/java/io/micronaut/kubernetes/client/DiscoveryCache.java b/kubernetes-client/src/main/java/io/micronaut/kubernetes/client/DiscoveryCache.java index 2c71eee78..2ce6c8d83 100644 --- a/kubernetes-client/src/main/java/io/micronaut/kubernetes/client/DiscoveryCache.java +++ b/kubernetes-client/src/main/java/io/micronaut/kubernetes/client/DiscoveryCache.java @@ -21,6 +21,7 @@ import io.kubernetes.client.openapi.ApiException; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; +import io.micronaut.retry.annotation.Retryable; import jakarta.inject.Inject; import jakarta.inject.Provider; import jakarta.inject.Singleton; @@ -112,7 +113,13 @@ public Optional find(Class cl } } - private Set getLastAPIDiscovery() throws ApiException { + /** + * Refreshes the cache if it is stale, otherwise returns cached {@link Discovery} results. + * + * @return api set of {@link Discovery.APIResource} + */ + @Retryable + Set getLastAPIDiscovery() throws ApiException { long nowMillis = System.currentTimeMillis(); if (nowMillis < nextDiscoveryRefreshTimeMillis) { return lastAPIDiscovery; diff --git a/kubernetes-client/src/test/groovy/io/micronaut/kubernetes/client/utils/KubernetesSpecification.groovy b/kubernetes-client/src/test/groovy/io/micronaut/kubernetes/client/utils/KubernetesSpecification.groovy index da9a1bfdd..303a85881 100644 --- a/kubernetes-client/src/test/groovy/io/micronaut/kubernetes/client/utils/KubernetesSpecification.groovy +++ b/kubernetes-client/src/test/groovy/io/micronaut/kubernetes/client/utils/KubernetesSpecification.groovy @@ -20,15 +20,18 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.KubernetesOperations -import jakarta.inject.Inject import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import jakarta.inject.Inject + /** * Abstract specification encapsulating setup of test namespace. * @@ -53,6 +56,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +105,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +217,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/kubernetes-client/src/test/resources/k8s/kubernetes-client-example-deployment.yml b/kubernetes-client/src/test/resources/k8s/kubernetes-client-example-deployment.yml index e7f8506e3..edae7c3e3 100644 --- a/kubernetes-client/src/test/resources/k8s/kubernetes-client-example-deployment.yml +++ b/kubernetes-client/src/test/resources/k8s/kubernetes-client-example-deployment.yml @@ -14,7 +14,7 @@ spec: containers: - name: kubernetes-client-example image: micronaut-kubernetes-client-example - imagePullPolicy: "Never" + imagePullPolicy: "IfNotPresent" ports: - name: http containerPort: 8085 diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigurationClient.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigurationClient.java index 60345740d..4abfcc9bc 100644 --- a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigurationClient.java +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigurationClient.java @@ -181,6 +181,7 @@ private Flux getPropertySourcesFromConfigMaps() { }) .flatMap(labelSelector -> client.listNamespacedConfigMap(configuration.getNamespace(), null, null, null, null, labelSelector, null, null, null, null)) + .retry(5) .doOnError(ApiException.class, throwable -> LOG.error("Error to list ConfigMaps in the namespace [" + configuration.getNamespace() + "]: " + throwable.getResponseBody(), throwable)) .onErrorResume(throwable -> exceptionOnPodLabelsMissing ? Mono.error(throwable) diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/KubernetesConfigurationSpec.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/KubernetesConfigurationSpec.groovy index a6985b7dc..438e759b6 100644 --- a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/KubernetesConfigurationSpec.groovy +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/KubernetesConfigurationSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.kubernetes import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Property import io.micronaut.context.env.Environment import io.micronaut.kubernetes.KubernetesConfiguration import io.micronaut.kubernetes.client.DefaultNamespaceResolver @@ -9,6 +10,8 @@ import spock.lang.Requires import spock.lang.Specification @Requires({ TestUtils.kubernetesApiAvailable() }) +@Property(name = "spec.reuseNamespace", value = "false") +@Property(name = "kubernetes.client.namespace", value = "kubernetes-configuration-spec") class KubernetesConfigurationSpec extends Specification { void "the namespace can be set via configuration"() { diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcherSpec.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcherSpec.groovy index a0e0866cd..2e735fa63 100644 --- a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcherSpec.groovy +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcherSpec.groovy @@ -1,5 +1,6 @@ package io.micronaut.kubernetes.configuration +import groovy.util.logging.Slf4j import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Property import io.micronaut.context.env.Environment @@ -12,6 +13,8 @@ import spock.lang.Specification @MicronautTest(environments = [Environment.KUBERNETES]) @Requires({ TestUtils.kubernetesApiAvailable() }) @Property(name = "spec.reuseNamespace", value = "false") +@Property(name = "kubernetes.client.namespace", value = "kubernetes-config-map-watcher-spec") +@Slf4j class KubernetesConfigMapWatcherSpec extends Specification { void "KubernetesConfigMapWatcher exists when informer is enabled"() { diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientFilterSpec.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientFilterSpec.groovy index 3104bb9ed..a4e927777 100644 --- a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientFilterSpec.groovy +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientFilterSpec.groovy @@ -1,6 +1,8 @@ package io.micronaut.kubernetes.configuration +import groovy.util.logging.Slf4j import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Property import io.micronaut.context.env.Environment import io.micronaut.kubernetes.utils.KubernetesSpecification import io.micronaut.kubernetes.test.TestUtils @@ -10,6 +12,9 @@ import spock.lang.Requires @MicronautTest(environments = [Environment.KUBERNETES]) @Requires({ TestUtils.kubernetesApiAvailable() }) +@Property(name = "spec.reuseNamespace", value = "false") +@Property(name = "kubernetes.client.namespace", value = "kubernetes-configuration-client-filter-spec") +@Slf4j class KubernetesConfigurationClientFilterSpec extends KubernetesSpecification { void setup() { diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientLabelsSpec.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientLabelsSpec.groovy index b7d11a36e..1fdc1a323 100644 --- a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientLabelsSpec.groovy +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientLabelsSpec.groovy @@ -1,8 +1,10 @@ package io.micronaut.kubernetes.configuration import com.github.stefanbirkner.systemlambda.SystemLambda +import groovy.util.logging.Slf4j import io.fabric8.kubernetes.api.model.Pod import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Property import io.micronaut.context.env.Environment import io.micronaut.context.exceptions.ConfigurationException import io.micronaut.kubernetes.utils.KubernetesSpecification @@ -13,6 +15,9 @@ import spock.lang.Requires @MicronautTest(environments = [Environment.KUBERNETES]) @Requires({ TestUtils.kubernetesApiAvailable() }) +@Property(name = "spec.reuseNamespace", value = "false") +@Property(name = "kubernetes.client.namespace", value = "kubernetes-configuration-client-labels-spec") +@Slf4j class KubernetesConfigurationClientLabelsSpec extends KubernetesSpecification { void setup() { @@ -82,7 +87,7 @@ class KubernetesConfigurationClientLabelsSpec extends KubernetesSpecification { Pod pod = TestUtils.getPods(namespace).find { it.metadata.labels && it.metadata.labels.containsKey("app.kubernetes.io/instance") } def envs = SystemLambda.withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "localhost") .and("HOSTNAME", pod.metadata.name) - ApplicationContext applicationContext = ApplicationContext.run(["kubernetes.client.config-maps.pod-labels": ["app.kubernetes.io/instance"]], Environment.KUBERNETES) + ApplicationContext applicationContext = ApplicationContext.run(["kubernetes.client.namespace": namespace, "kubernetes.client.config-maps.pod-labels": ["app.kubernetes.io/instance"]], Environment.KUBERNETES) KubernetesConfigurationClient configurationClient = applicationContext.getBean(KubernetesConfigurationClient) when: @@ -109,7 +114,7 @@ class KubernetesConfigurationClientLabelsSpec extends KubernetesSpecification { Pod pod = TestUtils.getPods(namespace).find { it.metadata.labels && it.metadata.labels.containsKey("app.kubernetes.io/instance") } def envs = SystemLambda.withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "localhost") .and("HOSTNAME", pod.metadata.name) - ApplicationContext applicationContext = ApplicationContext.run(["kubernetes.client.secrets.enabled": true, "kubernetes.client.secrets.pod-labels": ["app.kubernetes.io/instance"]], Environment.KUBERNETES) + ApplicationContext applicationContext = ApplicationContext.run(["kubernetes.client.namespace": namespace, "kubernetes.client.secrets.enabled": true, "kubernetes.client.secrets.pod-labels": ["app.kubernetes.io/instance"]], Environment.KUBERNETES) KubernetesConfigurationClient configurationClient = applicationContext.getBean(KubernetesConfigurationClient) when: @@ -131,7 +136,7 @@ class KubernetesConfigurationClientLabelsSpec extends KubernetesSpecification { Pod pod = TestUtils.getPods(namespace).find { it.metadata.labels && it.metadata.labels.containsKey("app.kubernetes.io/instance") } def envs = SystemLambda.withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "localhost") .and("HOSTNAME", pod.metadata.name) - ApplicationContext applicationContext = ApplicationContext.run(["kubernetes.client.config-maps.pod-labels": ["missing.label"], + ApplicationContext applicationContext = ApplicationContext.run(["kubernetes.client.namespace": namespace, "kubernetes.client.config-maps.pod-labels": ["missing.label"], "kubernetes.client.config-maps.exception-on-pod-labels-missing": "true"], Environment.KUBERNETES) KubernetesConfigurationClient configurationClient = applicationContext.getBean(KubernetesConfigurationClient) @@ -156,7 +161,7 @@ class KubernetesConfigurationClientLabelsSpec extends KubernetesSpecification { Pod pod = TestUtils.getPods(namespace).find { it.metadata.labels && it.metadata.labels.containsKey("app.kubernetes.io/instance") } def envs = SystemLambda.withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "localhost") .and("HOSTNAME", pod.metadata.name) - ApplicationContext applicationContext = ApplicationContext.run(["kubernetes.client.secrets.enabled": true, + ApplicationContext applicationContext = ApplicationContext.run(["kubernetes.client.namespace": namespace, "kubernetes.client.secrets.enabled": true, "kubernetes.client.secrets.pod-labels": ["missing.label"], "kubernetes.client.secrets.exception-on-pod-labels-missing": "true"], Environment.KUBERNETES) KubernetesConfigurationClient configurationClient = applicationContext.getBean(KubernetesConfigurationClient) diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientSecretSpec.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientSecretSpec.groovy index 817763b9c..c0edc049a 100644 --- a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientSecretSpec.groovy +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientSecretSpec.groovy @@ -1,8 +1,9 @@ package io.micronaut.kubernetes.configuration +import groovy.util.logging.Slf4j import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Property import io.micronaut.context.env.Environment -import io.micronaut.context.env.PropertySource import io.micronaut.kubernetes.utils.KubernetesSpecification import io.micronaut.kubernetes.test.TestUtils import io.micronaut.test.extensions.spock.annotation.MicronautTest @@ -11,6 +12,9 @@ import spock.lang.Requires @MicronautTest(environments = [Environment.KUBERNETES]) @Requires({ TestUtils.kubernetesApiAvailable() }) +@Property(name = "spec.reuseNamespace", value = "false") +@Property(name = "kubernetes.client.namespace", value = "kubernetes-configuration-client-secret-spec") +@Slf4j class KubernetesConfigurationClientSecretSpec extends KubernetesSpecification { void setup() { diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientSpec.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientSpec.groovy index bf645f741..040a02d30 100644 --- a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientSpec.groovy +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesConfigurationClientSpec.groovy @@ -1,6 +1,8 @@ package io.micronaut.kubernetes.configuration +import groovy.util.logging.Slf4j import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Property import io.micronaut.context.env.Environment import io.micronaut.context.env.EnvironmentPropertySource import io.micronaut.context.env.PropertySource @@ -15,6 +17,9 @@ import jakarta.inject.Inject @MicronautTest(environments = [Environment.KUBERNETES]) @Requires({ TestUtils.kubernetesApiAvailable() }) +@Property(name = "spec.reuseNamespace", value = "false") +@Property(name = "kubernetes.client.namespace", value = "kubernetes-configuration-client-spec") +@Slf4j class KubernetesConfigurationClientSpec extends KubernetesSpecification { @Inject diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientFilterSpec.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientFilterSpec.groovy index 698144b61..c1712b9e4 100644 --- a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientFilterSpec.groovy +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientFilterSpec.groovy @@ -18,6 +18,7 @@ import spock.util.concurrent.PollingConditions @MicronautTest(environments = [Environment.KUBERNETES]) @Requires({ TestUtils.kubernetesApiAvailable() }) @Property(name = "spec.reuseNamespace", value = "false") +@Property(name = "kubernetes.client.namespace", value = "kubernetes-discovery-client-filter-spec") class KubernetesDiscoveryClientFilterSpec extends KubernetesSpecification implements KubectlCommands { @Shared diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientLabelsSpec.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientLabelsSpec.groovy index f71e01ebf..3abf70749 100644 --- a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientLabelsSpec.groovy +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientLabelsSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.kubernetes.discovery import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Property import io.micronaut.context.env.Environment import io.micronaut.kubernetes.discovery.provider.KubernetesServiceInstanceEndpointProvider import io.micronaut.kubernetes.discovery.provider.KubernetesServiceInstanceServiceProvider @@ -14,6 +15,8 @@ import spock.util.concurrent.PollingConditions @MicronautTest(environments = [Environment.KUBERNETES]) @Requires({ TestUtils.kubernetesApiAvailable() }) +@Property(name = "spec.reuseNamespace", value = "false") +@Property(name = "kubernetes.client.namespace", value = "kubernetes-ciscovery-client-labels-spec") class KubernetesDiscoveryClientLabelsSpec extends KubernetesSpecification { @Shared diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientSpec.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientSpec.groovy index 25be83342..e951be2cd 100644 --- a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientSpec.groovy +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/discovery/KubernetesDiscoveryClientSpec.groovy @@ -16,6 +16,7 @@ package io.micronaut.kubernetes.discovery +import io.micronaut.context.annotation.Property import io.micronaut.context.env.Environment import io.micronaut.discovery.ServiceInstance import io.micronaut.kubernetes.utils.KubernetesSpecification @@ -31,6 +32,8 @@ import java.util.stream.Stream @MicronautTest(environments = [Environment.KUBERNETES]) @Requires({ TestUtils.kubernetesApiAvailable() }) +@Property(name = "spec.reuseNamespace", value = "false") +@Property(name = "kubernetes.client.namespace", value = "kubernetes-discovery-client-spec") class KubernetesDiscoveryClientSpec extends KubernetesSpecification{ @Inject diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/utils/KubernetesSpecification.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/utils/KubernetesSpecification.groovy index 6f2fd3527..ccea2cef6 100644 --- a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/utils/KubernetesSpecification.groovy +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/utils/KubernetesSpecification.groovy @@ -20,15 +20,18 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.KubernetesOperations -import jakarta.inject.Inject import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import jakarta.inject.Inject + /** * Abstract specification encapsulating setup of test namespace. * @@ -53,6 +56,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +105,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +217,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/kubernetes-informer/src/test/groovy/io/micronaut/kubernetes/client/informer/utils/KubernetesSpecification.groovy b/kubernetes-informer/src/test/groovy/io/micronaut/kubernetes/client/informer/utils/KubernetesSpecification.groovy index 11cc6fc7b..18a4d75d9 100644 --- a/kubernetes-informer/src/test/groovy/io/micronaut/kubernetes/client/informer/utils/KubernetesSpecification.groovy +++ b/kubernetes-informer/src/test/groovy/io/micronaut/kubernetes/client/informer/utils/KubernetesSpecification.groovy @@ -20,15 +20,18 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.KubernetesOperations -import jakarta.inject.Inject import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import jakarta.inject.Inject + /** * Abstract specification encapsulating setup of test namespace. * @@ -53,6 +56,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +105,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +217,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/kubernetes-operator/src/test/groovy/io/micronaut/kubernetes/client/operator/utils/KubernetesSpecification.groovy b/kubernetes-operator/src/test/groovy/io/micronaut/kubernetes/client/operator/utils/KubernetesSpecification.groovy index 6fdbe6dfa..247bfbfc1 100644 --- a/kubernetes-operator/src/test/groovy/io/micronaut/kubernetes/client/operator/utils/KubernetesSpecification.groovy +++ b/kubernetes-operator/src/test/groovy/io/micronaut/kubernetes/client/operator/utils/KubernetesSpecification.groovy @@ -20,15 +20,18 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import io.micronaut.kubernetes.test.KubernetesOperations -import jakarta.inject.Inject import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import jakarta.inject.Inject + /** * Abstract specification encapsulating setup of test namespace. * @@ -53,6 +56,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +105,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +217,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/kubernetes.yml b/kubernetes.yml index dfdcff2d0..2c675bf20 100644 --- a/kubernetes.yml +++ b/kubernetes.yml @@ -16,7 +16,7 @@ spec: containers: - name: example-service image: micronaut-kubernetes-example-service - imagePullPolicy: "Never" + imagePullPolicy: "IfNotPresent" volumeMounts: - name: secrets mountPath: "/etc/example-service/secrets" @@ -68,7 +68,7 @@ spec: containers: - name: example-client image: micronaut-kubernetes-example-client - imagePullPolicy: "Never" + imagePullPolicy: "IfNotPresent" env: - name: JAVA_TOOL_OPTIONS value: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" diff --git a/setup-kind.sh b/setup-kind.sh new file mode 100755 index 000000000..f054e9208 --- /dev/null +++ b/setup-kind.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -ex + +# +# Defaults +K8S_DEFAULT_VERSION="1.21" +KUBECTL_DEFAULT_VERSION="v1.19.2" +KIND_VERSION="v0.11.1" +EXAMPLE_SERVICE_RUNTIME=${EXAMPLE_SERVICE_RUNTIME:="java"} + +# +# K8s images required for KIND version +K8S_121="kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6" +K8S_120="kindest/node:v1.20.7@sha256:cbeaf907fc78ac97ce7b625e4bf0de16e3ea725daf6b04f930bd14c67c671ff9" +K8S_119="kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729" +K8S_118="kindest/node:v1.18.19@sha256:7af1492e19b3192a79f606e43c35fb741e520d195f96399284515f077b3b622c" +K8S_117="kindest/node:v1.17.17@sha256:66f1d0d91a88b8a001811e2f1054af60eef3b669a9a74f9b6db871f2f1eeed00" + +# +# Resolve K8s version +K8S_VERSION=${K8S_VERSION:=$K8S_DEFAULT_VERSION} +echo "K8S_VERSION = $K8S_VERSION" + +# +# Resolve kind version +echo "KIND_VERSION = $KIND_VERSION" + +# +# Resolve kubectl version +KUBECTL_VERSION=$(curl -X GET -s https://api.github.com/repos/kubernetes/kubernetes/releases | jq -r "[.[]| select(.name | match(\"v$K8S_VERSION.[0-9]+$\")).name][0]" ) +if [[ $KUBECTL_VERSION != v${K8S_VERSION}* ]]; then + echo "Resolved KUBECTL_VERSION: $KUBECTL_VERSION doesn't start with v$K8S_VERSION, defaults to $KUBECTL_DEFAULT_VERSION" + KUBECTL_VERSION=$KUBECTL_DEFAULT_VERSION +fi +echo "KUBECTL_VERSION = $KUBECTL_VERSION" + +# +# Resolve kind node image +if [[ "1.21" == "${K8S_VERSION}" ]]; then + KIND_NODE_IMAGE_VERSION=$K8S_121 +elif [[ "1.20" == "${K8S_VERSION}" ]]; then + KIND_NODE_IMAGE_VERSION=$K8S_120 +elif [[ "1.19" == "${K8S_VERSION}" ]]; then + KIND_NODE_IMAGE_VERSION=$K8S_119 +elif [[ "1.18" == "${K8S_VERSION}" ]]; then + KIND_NODE_IMAGE_VERSION=$K8S_118 +elif [[ "1.17" == "${K8S_VERSION}" ]]; then + KIND_NODE_IMAGE_VERSION=$K8S_117 +fi +echo "KIND_NODE_IMAGE_VERSION = $KIND_NODE_IMAGE_VERSION" + +# +# Download and install kubectl +curl -LO https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl && chmod +x ./kubectl && mv ./kubectl ${HOME}/kubectl + +# +# Download and install kind +curl -Lo ${HOME}/kind "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-$(uname)-amd64" && chmod +x ${HOME}/kind + +export PATH="$PATH:${HOME}" + +# +# Create a cluster +KIND_CLUSTER=$(echo $K8S_VERSION | tr -cd '[:alnum:]') +KIND_CLUSTER_NAME="k8s${KIND_CLUSTER}java${JAVA_VERSION}" +$HOME/kind create cluster --name ${KIND_CLUSTER_NAME} --image ${KIND_NODE_IMAGE_VERSION} --wait 5m + +# Test the cluster was created +$HOME/kubectl get ns kube-system || exit 1 + +$HOME/kubectl cluster-info +$HOME/kubectl version + +sh setup-kubernetes-kind.sh -c "${KIND_CLUSTER_NAME}" -t "${EXAMPLE_SERVICE_RUNTIME}" \ No newline at end of file diff --git a/setup-kubernetes-kind.sh b/setup-kubernetes-kind.sh new file mode 100755 index 000000000..aec039560 --- /dev/null +++ b/setup-kubernetes-kind.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env sh +###################################### +# Setup script for local developemnt # +###################################### +set -ex + +while getopts c:t: flag; do + case "${flag}" in + c) CLUSTER_OPTION=${OPTARG} ;; + t) TYPE_OPTION=${OPTARG} ;; + *) ;; + esac +done + +CLUSTER_NAME=${CLUSTER_OPTION:-kind} +TYPE=${TYPE_OPTION:-java} + +if [ "$TYPE" = "java" ]; then + TYPE_CMD="dockerBuild" +elif [ "$TYPE" = "native" ]; then + TYPE_CMD="dockerBuildNative" +fi + +kind get clusters + +./gradlew clean $TYPE_CMD --refresh-dependencies +kind --name "$CLUSTER_NAME" load docker-image micronaut-kubernetes-example-service:latest +kind --name "$CLUSTER_NAME" load docker-image micronaut-kubernetes-example-client:latest +kind --name "$CLUSTER_NAME" load docker-image micronaut-kubernetes-client-example:latest +kind --name "$CLUSTER_NAME" load docker-image micronaut-kubernetes-informer-example:latest +kind --name "$CLUSTER_NAME" load docker-image micronaut-kubernetes-operator-example:latest + +# +# Run Kubernetes API proxy +pkill -9 kubectl || true +kubectl proxy & diff --git a/setup-kubernetes.sh b/setup-kubernetes.sh index aec039560..1a0e260e8 100755 --- a/setup-kubernetes.sh +++ b/setup-kubernetes.sh @@ -1,8 +1,9 @@ -#!/usr/bin/env sh +#!/bin/bash ###################################### # Setup script for local developemnt # ###################################### set -ex +set -euxo pipefail while getopts c:t: flag; do case "${flag}" in @@ -12,7 +13,7 @@ while getopts c:t: flag; do esac done -CLUSTER_NAME=${CLUSTER_OPTION:-kind} +CLUSTER_NAME=${CLUSTER_OPTION:-micronaut-k8s} TYPE=${TYPE_OPTION:-java} if [ "$TYPE" = "java" ]; then @@ -21,14 +22,36 @@ elif [ "$TYPE" = "native" ]; then TYPE_CMD="dockerBuildNative" fi -kind get clusters +TAG_NAME="$TYPE-$JAVA_VERSION-${GIT_COMMIT_HASH:-latest}" ./gradlew clean $TYPE_CMD --refresh-dependencies -kind --name "$CLUSTER_NAME" load docker-image micronaut-kubernetes-example-service:latest -kind --name "$CLUSTER_NAME" load docker-image micronaut-kubernetes-example-client:latest -kind --name "$CLUSTER_NAME" load docker-image micronaut-kubernetes-client-example:latest -kind --name "$CLUSTER_NAME" load docker-image micronaut-kubernetes-informer-example:latest -kind --name "$CLUSTER_NAME" load docker-image micronaut-kubernetes-operator-example:latest + +docker login $OCI_CLI_REGION.ocir.io -u $OCIR_USERNAME -p $AUTH_TOKEN + +OCIR_REPOSITORY="$OCI_CLI_REGION.ocir.io/$OCI_TENANCY_NAME" + +arr=("micronaut-kubernetes-example-service" "micronaut-kubernetes-example-client" "micronaut-kubernetes-client-example" "micronaut-kubernetes-informer-example" "micronaut-kubernetes-operator-example") + +## now loop through the above array +for image in ${arr[@]}; do + docker tag "$image:latest" "$OCIR_REPOSITORY/$image:$TAG_NAME" + docker push "$OCIR_REPOSITORY/$image:$TAG_NAME" +done + +sed -i -e "s|micronaut-kubernetes-client-example|${OCIR_REPOSITORY}/micronaut-kubernetes-client-example:${TAG_NAME}|g" kubernetes-client/src/test/resources/k8s/kubernetes-client-example-deployment.yml + +sed -i -e "s|micronaut-kubernetes-example-service|${OCIR_REPOSITORY}/micronaut-kubernetes-example-service:${TAG_NAME}|g" kubernetes.yml +sed -i -e "s|micronaut-kubernetes-example-client|${OCIR_REPOSITORY}/micronaut-kubernetes-example-client:${TAG_NAME}|g" kubernetes.yml + +sed -i -e "s|micronaut-kubernetes-example-client|${OCIR_REPOSITORY}/micronaut-kubernetes-example-client:${TAG_NAME}|g" test-utils/src/main/resources/k8s/example-client-deployment.yml +sed -i -e "s|micronaut-kubernetes-example-service|${OCIR_REPOSITORY}/micronaut-kubernetes-example-service:${TAG_NAME}|g" test-utils/src/main/resources/k8s/example-service-deployment.yml + +# use nginx image from OCIR because of rate limit +sed -i -e "s|nginx|${OCIR_REPOSITORY}/nginx|g" test-utils/src/main/resources/k8s/secure-deployment.yml +sed -i -e "s|nginx|${OCIR_REPOSITORY}/nginx|g" test-utils/src/test/resources/k8s/deployment.yml +sed -i -e "s|nginx|${OCIR_REPOSITORY}/nginx|g" kubernetes.yml + +grep 'Switched active kube context' vcluster-out.log # # Run Kubernetes API proxy diff --git a/setup.sh b/setup.sh index 3b76e8a90..3078c54a3 100755 --- a/setup.sh +++ b/setup.sh @@ -1,74 +1,27 @@ #!/bin/bash set -ex -# # Defaults -K8S_DEFAULT_VERSION="1.21" -KUBECTL_DEFAULT_VERSION="v1.19.2" -KIND_VERSION="v0.11.1" EXAMPLE_SERVICE_RUNTIME=${EXAMPLE_SERVICE_RUNTIME:="java"} -# -# K8s images required for KIND version -K8S_121="kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6" -K8S_120="kindest/node:v1.20.7@sha256:cbeaf907fc78ac97ce7b625e4bf0de16e3ea725daf6b04f930bd14c67c671ff9" -K8S_119="kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729" -K8S_118="kindest/node:v1.18.19@sha256:7af1492e19b3192a79f606e43c35fb741e520d195f96399284515f077b3b622c" -K8S_117="kindest/node:v1.17.17@sha256:66f1d0d91a88b8a001811e2f1054af60eef3b669a9a74f9b6db871f2f1eeed00" +# Download vcluster CLI +curl -L -o vcluster "https://github.com/loft-sh/vcluster/releases/latest/download/vcluster-linux-amd64" && sudo install -c -m 0755 vcluster /usr/local/bin && rm -f vcluster -# -# Resolve K8s version -K8S_VERSION=${K8S_VERSION:=$K8S_DEFAULT_VERSION} -echo "K8S_VERSION = $K8S_VERSION" +#Print vcluster version +vcluster --version -# -# Resolve kind version -echo "KIND_VERSION = $KIND_VERSION" +kubectl get ns kube-system || exit 1 -# -# Resolve kubectl version -KUBECTL_VERSION=$(curl -X GET -s https://api.github.com/repos/kubernetes/kubernetes/releases | jq -r "[.[]| select(.name | match(\"v$K8S_VERSION.[0-9]+$\")).name][0]" ) -if [[ $KUBECTL_VERSION != v${K8S_VERSION}* ]]; then - echo "Resolved KUBECTL_VERSION: $KUBECTL_VERSION doesn't start with v$K8S_VERSION, defaults to $KUBECTL_DEFAULT_VERSION" - KUBECTL_VERSION=$KUBECTL_DEFAULT_VERSION -fi -echo "KUBECTL_VERSION = $KUBECTL_VERSION" +CLUSTER_NAME="micronaut-${JOB_ID:-k8s-cluster}" -# -# Resolve kind node image -if [[ "1.21" == "${K8S_VERSION}" ]]; then - KIND_NODE_IMAGE_VERSION=$K8S_121 -elif [[ "1.20" == "${K8S_VERSION}" ]]; then - KIND_NODE_IMAGE_VERSION=$K8S_120 -elif [[ "1.19" == "${K8S_VERSION}" ]]; then - KIND_NODE_IMAGE_VERSION=$K8S_119 -elif [[ "1.18" == "${K8S_VERSION}" ]]; then - KIND_NODE_IMAGE_VERSION=$K8S_118 -elif [[ "1.17" == "${K8S_VERSION}" ]]; then - KIND_NODE_IMAGE_VERSION=$K8S_117 -fi -echo "KIND_NODE_IMAGE_VERSION = $KIND_NODE_IMAGE_VERSION" - -# -# Download and install kubectl -curl -LO https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl && chmod +x ./kubectl && mv ./kubectl ${HOME}/kubectl - -# -# Download and install kind -curl -Lo ${HOME}/kind "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-$(uname)-amd64" && chmod +x ${HOME}/kind - -export PATH="$PATH:${HOME}" - -# -# Create a cluster -KIND_CLUSTER=$(echo $K8S_VERSION | tr -cd '[:alnum:]') -KIND_CLUSTER_NAME="k8s${KIND_CLUSTER}java${JAVA_VERSION}" -$HOME/kind create cluster --name ${KIND_CLUSTER_NAME} --image ${KIND_NODE_IMAGE_VERSION} --wait 5m +# create cluster +vcluster create "$CLUSTER_NAME" 2>&1 > vcluster-out.log & # Test the cluster was created -$HOME/kubectl get ns kube-system || exit 1 +kubectl get ns kube-system || exit 1 + +kubectl cluster-info -$HOME/kubectl cluster-info -$HOME/kubectl version +kubectl version -sh setup-kubernetes.sh -c "${KIND_CLUSTER_NAME}" -t "${EXAMPLE_SERVICE_RUNTIME}" \ No newline at end of file +bash setup-kubernetes.sh -c "${CLUSTER_NAME}" -t "${EXAMPLE_SERVICE_RUNTIME}" \ No newline at end of file diff --git a/teardown-kind.sh b/teardown-kind.sh new file mode 100755 index 000000000..ba71279e1 --- /dev/null +++ b/teardown-kind.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -ex + +# +# Defaults +K8S_DEFAULT_VERSION="1.19" + +# +# Resolve K8s version +K8S_VERSION=${K8S_VERSION:=$K8S_DEFAULT_VERSION} +echo "K8S_VERSION = $K8S_VERSION" + +# +# Create a cluster +KIND_CLUSTER=$(echo $K8S_VERSION | tr -cd '[:alnum:]') +KIND_CLUSTER_NAME="k8s${KIND_CLUSTER}java${JAVA_VERSION}" +./kind delete cluster --name ${KIND_CLUSTER_NAME} || true + +# +# Stop kubernetes API proxy +pkill -9 kubectl || true \ No newline at end of file diff --git a/teardown.sh b/teardown.sh index ba71279e1..36e6ab5e2 100755 --- a/teardown.sh +++ b/teardown.sh @@ -3,19 +3,11 @@ set -ex # # Defaults -K8S_DEFAULT_VERSION="1.19" +CLUSTER_NAME="micronaut-${JOB_ID:-k8s-cluster}" -# -# Resolve K8s version -K8S_VERSION=${K8S_VERSION:=$K8S_DEFAULT_VERSION} -echo "K8S_VERSION = $K8S_VERSION" - -# -# Create a cluster -KIND_CLUSTER=$(echo $K8S_VERSION | tr -cd '[:alnum:]') -KIND_CLUSTER_NAME="k8s${KIND_CLUSTER}java${JAVA_VERSION}" -./kind delete cluster --name ${KIND_CLUSTER_NAME} || true +vcluster delete "$CLUSTER_NAME" -# # Stop kubernetes API proxy -pkill -9 kubectl || true \ No newline at end of file +pkill -9 kubectl || true +# Stop vcluster +pkill -9 vcluster || true \ No newline at end of file diff --git a/test-utils/build.gradle b/test-utils/build.gradle index 4d8c05ff4..f607d4d23 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -14,4 +14,8 @@ dependencies { api libs.spock.core api mnTest.micronaut.test.spock api mnLogging.logback.classic + api mn.micronaut.management + implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' + implementation 'org.bouncycastle:bcprov-jdk15on:1.70' + implementation 'org.bouncycastle:bcprov-ext-jdk15on:1.70' } diff --git a/test-utils/src/main/groovy/io/micronaut/kubernetes/test/KubernetesOperations.groovy b/test-utils/src/main/groovy/io/micronaut/kubernetes/test/KubernetesOperations.groovy index 80a7551ae..d41377d98 100644 --- a/test-utils/src/main/groovy/io/micronaut/kubernetes/test/KubernetesOperations.groovy +++ b/test-utils/src/main/groovy/io/micronaut/kubernetes/test/KubernetesOperations.groovy @@ -20,10 +20,9 @@ import io.fabric8.kubernetes.api.model.* import io.fabric8.kubernetes.api.model.apps.Deployment import io.fabric8.kubernetes.api.model.rbac.* import io.fabric8.kubernetes.client.ConfigBuilder -import io.fabric8.kubernetes.client.DefaultKubernetesClient import io.fabric8.kubernetes.client.KubernetesClient +import io.fabric8.kubernetes.client.KubernetesClientBuilder import io.fabric8.kubernetes.client.LocalPortForward -import io.fabric8.kubernetes.client.dsl.RollableScalableResource import io.micronaut.core.util.StringUtils import spock.util.concurrent.PollingConditions @@ -46,7 +45,14 @@ class KubernetesOperations implements Closeable { KubernetesClient getClient(String namespace = 'default') { return kubernetesClientMap.computeIfAbsent(namespace, ns -> - new DefaultKubernetesClient(new ConfigBuilder().withNamespace(ns).build()) + new KubernetesClientBuilder() + .withConfig(new ConfigBuilder().withTrustCerts(true) + .withNamespace(ns) + .withRequestRetryBackoffLimit(5) + .withRequestTimeout(30000) + .build() + ).build() + ) } @@ -76,14 +82,15 @@ class KubernetesOperations implements Closeable { } boolean deleteNamespace(String name) { - log.debug("Deleting namespace ${name}") - getClient().namespaces().delete(getNamespace(name)) + log.info("Deleting namespace ${name}") + getClient().namespaces().resource(getNamespace(name)).delete() def waitTime = 3000 while (true) { def namespaces = getClient().namespaces().list().items.stream() .map(it -> it.metadata.name).collect(Collectors.toList()) if (namespaces.contains(name)) { log.info("Namespace ${namespaces} still exists, sleeping for ${waitTime / 1000} seconds...") + sleep(waitTime) } else { log.info("Namespace sucessfully deleted: ${namespaces}") break diff --git a/test-utils/src/main/groovy/io/micronaut/kubernetes/test/KubernetesSpecification.groovy b/test-utils/src/main/groovy/io/micronaut/kubernetes/test/KubernetesSpecification.groovy index 66882a760..e0d051871 100644 --- a/test-utils/src/main/groovy/io/micronaut/kubernetes/test/KubernetesSpecification.groovy +++ b/test-utils/src/main/groovy/io/micronaut/kubernetes/test/KubernetesSpecification.groovy @@ -20,9 +20,11 @@ import io.fabric8.kubernetes.api.model.IntOrString import io.fabric8.kubernetes.api.model.ServicePortBuilder import io.fabric8.kubernetes.api.model.ServiceSpecBuilder import io.fabric8.kubernetes.api.model.apps.Deployment +import io.micronaut.context.annotation.Property import io.micronaut.context.annotation.Value import io.micronaut.core.io.ResourceResolver import io.micronaut.core.io.scan.ClassPathResourceLoader +import io.micronaut.core.util.StringUtils import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -53,6 +55,14 @@ abstract class KubernetesSpecification extends Specification { @Value('${spec.reuseNamespace:true}') boolean reuseNamespace + @Property(name = "image.tag") + @Shared + Optional imageTag + + @Property(name = "image.prefix") + @Shared + Optional imagePrefix + /** * Setup the fixture in namespace. * @param namespace @@ -94,6 +104,7 @@ abstract class KubernetesSpecification extends Specification { log.info("Cleaning up namespace ${namespace}") operations.deleteNamespace(namespace) } + log.info("Finished cleaning") } def createBaseResources(String namespace) { @@ -205,6 +216,21 @@ abstract class KubernetesSpecification extends Specification { .build()) } + def getImageName(String imageName) { + def tagName = "latest" + + if (StringUtils.isNotEmpty(imagePrefix.orElse(null))) { + imageName = imagePrefix.get() + imageName + } + + if (StringUtils.isNotEmpty(imageTag.orElse(null))) { + tagName = imageTag.get() + } + + imageName = imageName + ":" + tagName + return imageName + } + static String encodeSecret(String secret) { return Base64.encoder.encodeToString(secret.bytes) } diff --git a/test-utils/src/main/resources/k8s/example-client-deployment.yml b/test-utils/src/main/resources/k8s/example-client-deployment.yml index 9ad267fc5..f22c05fcf 100644 --- a/test-utils/src/main/resources/k8s/example-client-deployment.yml +++ b/test-utils/src/main/resources/k8s/example-client-deployment.yml @@ -14,7 +14,7 @@ spec: containers: - name: example-client image: micronaut-kubernetes-example-client - imagePullPolicy: "Never" + imagePullPolicy: "IfNotPresent" env: - name: JAVA_TOOL_OPTIONS value: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" diff --git a/test-utils/src/main/resources/k8s/example-service-deployment.yml b/test-utils/src/main/resources/k8s/example-service-deployment.yml index 37dbd0649..d7d6e0e3b 100644 --- a/test-utils/src/main/resources/k8s/example-service-deployment.yml +++ b/test-utils/src/main/resources/k8s/example-service-deployment.yml @@ -16,7 +16,7 @@ spec: containers: - name: example-service image: micronaut-kubernetes-example-service - imagePullPolicy: "Never" + imagePullPolicy: "IfNotPresent" volumeMounts: - name: secrets mountPath: /etc/example-service/secrets diff --git a/test-utils/src/test/groovy/io/micronaut/kubernetes/test/KubernetesOperationsSpec.groovy b/test-utils/src/test/groovy/io/micronaut/kubernetes/test/KubernetesOperationsSpec.groovy index 145121d85..8b4f94d6c 100644 --- a/test-utils/src/test/groovy/io/micronaut/kubernetes/test/KubernetesOperationsSpec.groovy +++ b/test-utils/src/test/groovy/io/micronaut/kubernetes/test/KubernetesOperationsSpec.groovy @@ -1,6 +1,8 @@ package io.micronaut.kubernetes.test +import io.fabric8.kubernetes.client.KubernetesClientException import org.yaml.snakeyaml.Yaml +import spock.lang.AutoCleanup import spock.lang.Requires import spock.lang.Shared import spock.lang.Specification @@ -11,6 +13,7 @@ import java.nio.file.Paths class KubernetesOperationsSpec extends Specification{ @Shared + @AutoCleanup KubernetesOperations operations = new KubernetesOperations() def setupSpec() { @@ -19,8 +22,19 @@ class KubernetesOperationsSpec extends Specification{ } def cleanupSpec() { + def retry = 3 if (operations.getNamespace("test-namespace") != null) { - operations.deleteNamespace("test-namespace") + for (int i=0; i